Skip to content

Add IComponentActivator#23607

Merged
SteveSandersonMS merged 4 commits intomasterfrom
pr/19642
Jul 7, 2020
Merged

Add IComponentActivator#23607
SteveSandersonMS merged 4 commits intomasterfrom
pr/19642

Conversation

@SteveSandersonMS
Copy link
Member

@SteveSandersonMS SteveSandersonMS commented Jul 2, 2020

This is an updated version of PR #19642 by @stsrki. Please see the original PR for context and earlier discussion.

Fixes #7962

@SteveSandersonMS SteveSandersonMS force-pushed the pr/19642 branch 2 times, most recently from c75e6c9 to ea23d19 Compare July 2, 2020 12:17
@SteveSandersonMS SteveSandersonMS requested a review from a team July 2, 2020 13:27
@SteveSandersonMS SteveSandersonMS marked this pull request as ready for review July 2, 2020 13:27
@SteveSandersonMS
Copy link
Member Author

Another CR note: this PR changes what was previously an invariant of the system. Until now, we always knew for sure that a rendertree frame of type "Component" with ComponentType = T for some type T would always correspond to a component instance of type T.

After this PR, that invariant no longer holds, since the activator could return a component instance of any IComponent type, not necessarily T.

Loosening this invariant is beneficial for cases like testing as it means you can "mock" a child component or do shallow rendering (replace all the child components with empty things). However it's something to bear in mind in the future and not get confused about!

@Pilchie Pilchie added the area-blazor Includes: Blazor, Razor Components label Jul 2, 2020
/// <inheritdoc />
public IComponent CreateInstance(Type componentType)
{
var instance = Activator.CreateInstance(componentType);
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated to your change, but it might be worth profiling this to see if caching the factories makes a difference.

Copy link
Member Author

Choose a reason for hiding this comment

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

We only create one per Renderer so it seems very unlikely to make a difference to anything. Was there a particular reason you’re suspicious it might have an observable effect?

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh you mean some kind of cached activator? I’m not even sure that’s possible without Reflection.Emit (which works but is slow on WebAssembly). Do you know of a good way to implement it?

Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to suggest ActivatorUtiltiies.CreateFactory but that uses Reflection.Emit. I guess once levi's ObjectFactory PR lands, we can revisit. Oh well 😞

Copy link
Member Author

@SteveSandersonMS SteveSandersonMS Jul 3, 2020

Choose a reason for hiding this comment

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

It's probably possible to do it like this:

internal interface ICachedActivator
{
    object CreateInstance();
}

internal class CachedActivator<T> : ICachedActivator where T: class, new()
{
    public object CreateInstance() => new T();
}

... and then separately create a Dictionary<Type, ICachedActivator> where for each Type we construct an instance one time using reflection.

However I have absolutely no reason to think this would be faster than Activator.CreateInstance when running on an interpreter. It's probably slower. With the interpreter, the only difference between Activator.CreateInstance(typeof(T)) and new T() is having to look up the default constructor for T (assuming Activator.CreateInstance is an intrinsic), which most likely takes less time than finding typeof(T) in a dictionary keyed on that. I don't know how ObjectFactory is implemented but it's hard to see how it would be faster than Activator.CreateInstance on an interpreter.

Copy link
Contributor

Choose a reason for hiding this comment

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

@SteveSandersonMS
Copy link
Member Author

Pinging @dotnet/aspnet-blazor-eng for sign-off.

@SteveSandersonMS SteveSandersonMS requested a review from a team July 6, 2020 17:02
Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

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

Could you send an email over API-review? This API has precedence, since it's pretty close to how MVC's activator works. Outside of the few changes, this looks great.

/// <inheritdoc />
public IComponent CreateInstance(Type componentType)
{
var instance = Activator.CreateInstance(componentType);
Copy link
Contributor

Choose a reason for hiding this comment

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

@SteveSandersonMS SteveSandersonMS merged commit 0ec15ea into master Jul 7, 2020
@SteveSandersonMS SteveSandersonMS deleted the pr/19642 branch July 7, 2020 18:27
@SteveSandersonMS SteveSandersonMS added the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Jul 7, 2020
@ghost
Copy link

ghost commented Jul 7, 2020

Thank you for submitting this for API review. This will be reviewed by @dotnet/aspnet-api-review at the next meeting of the ASP.NET Core API Review group. Please ensure you take a look at the API review process documentation and ensure that:

  • The PR contains changes to the reference-assembly that describe the API change. Or, you have included a snippet of reference-assembly-style code that illustrates the API change.
  • The PR describes the impact to users, both positive (useful new APIs) and negative (breaking changes).
  • Someone is assigned to "champion" this change in the meeting, and they understand the impact and design of the change.

@stsrki
Copy link
Contributor

stsrki commented Jul 7, 2020

@SteveSandersonMS With this PR merged I will finally be able to do what I wanted long time ago within my component library. I cannot thank you enough time for helping me. I really appreciate all your work. Thank you!

@egil
Copy link
Contributor

egil commented Jul 7, 2020

@stsrki I am curious what you will use this feature for.

@SteveSandersonMS will this feature be added to the 3.x versions of Blazor as well, or will it only be available in the .net 5 version? Either way I guess I need to figure out how to do multi targeting correctly with bUnit.

@SteveSandersonMS
Copy link
Member Author

It’s for 5.0 only.

@stsrki Thanks for your help in making this happen too!

@stsrki
Copy link
Contributor

stsrki commented Jul 7, 2020

@egil Basically in my components library Blazorise every component in the root project can be overriden and customized by any the supported frameworks(Bootstrap, Material, AntDesign, etc.) But since untll now I didn't have freedom to create the components, I had to come up with some workarounds. Like, whenever any component is created and rendered for the first time I would check if there exists custom implementation. In case it exists it would render that one instead.

The problem was that I would basically create two components instead if just one. So with this PR I would be able to finally do that.

@egil
Copy link
Contributor

egil commented Jul 7, 2020

@stsrki ahh I see. So for example, you have a public <Button/> component, which internally would have a <BootstrapButton/>, a <MaterialButton/>, etc. implementations, which you choose at runtime. I have done something similar before, just with differnet renderfragments depending on the context. I see how this will enable a more clean and simple approach.

I wonder if the public <Button /> component in the scenario above could be an interface that inherits from IComponent. Would Blazor complain about this?

@stsrki
Copy link
Contributor

stsrki commented Jul 7, 2020

@egil Yeah you got it. That's how it works.

Well in theory it could be an interface but there is a lot more features and implementations behind the scenes. Just making it an interface would not suffice.

@egil
Copy link
Contributor

egil commented Jul 7, 2020

@stsrki no I guess you have shared logic between them, so an interface might not be ideal. I was more wondering if it would be allowed by the Blazor compiler.

@pranavkm pranavkm added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews labels Jul 13, 2020
@springy76
Copy link

@egil using plain C# you can render both abstract and interface types (when handling those by implementing a capable IComponentActivator):

    private static RenderFragment RenderAbstractComponent(int arg1) => builder =>
    {
        builder.OpenComponent<IJustTestComponent>(1);
        builder.AddAttribute(2, nameof(IJustTestComponent.Arg1), arg1);
        builder.CloseComponent();
    };

It's only the razor interpreter which starts acting stupid as soon as the component class is abstract or an interface:

    <JustTestComponentBase Arg1="123" />

and lies on this with unnecessary false errors:

error RZ10012: Found markup element with unexpected name 'JustTestComponentBase'.

This is frustating and imho if the referenced type finally implements IComponent the razor compiler should just leave it to the runtime as any other argument typo or argument-left-over just crashes at runtime when it doesn't match - or missing ParameterAttribute also just waits for the user at runtime to find the error.

@ghost
Copy link

ghost commented Feb 20, 2023

Hi @springy76. It looks like you just commented on a closed PR. The team will most probably miss it. If you'd like to bring something important up to their attention, consider filing a new issue and add enough details to build context.

@github-actions github-actions bot locked and limited conversation to collaborators Dec 8, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

api-approved API was approved in API review, it can be implemented area-blazor Includes: Blazor, Razor Components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow custom component instantiation

7 participants