dotnet / aspnetcore

ASP.NET Core is a cross-platform .NET framework for building modern cloud-based web applications on Windows, Mac, or Linux.
https://asp.net
MIT License
35.37k stars 9.99k forks source link

Model for referencing a component from another component - declaratively #51340

Closed htmlsplash closed 1 year ago

htmlsplash commented 1 year ago

Is there an existing issue for this?

Is your feature request related to a problem? Please describe the problem.

I would like component B to be able to access component A declaratively.

Ex 1: Consider this markup (using the ref attribute):

ComponentA @ref="compA" 
ComponentB ComponentA="compA"  // where "ComponentA" is a Parameter on Component B

code {
  ComponentA compA;
}

Rule: As I understand, this will not work because accessing components from life cycle methods except "OnAfterRender" is not possible.

1) Q: How do I achieve something similar what's shown in the example in Blazor? Accessing controls by ID in webforms is a common pattern.

2) Q: Why is the rule (that I stated) not followed consistently about component references.

Ex 2: Consider this markup:

<input @ref="inputField" />

code
{
protected override void OnInitialized()
{
    base.OnInitialized();
      var refToInputField = inputField; // The reference exists, but this will not work for components; Why is there this inconsistency? Is this because I am referencing an HTML element instead of Blazor component?
}
}

3) Current documentation is either off or not explicit about this: Goto: https://learn.microsoft.com/en-us/dotnet/architecture/blazor-for-web-forms-developers/components Look under heading: Capture component references This example is incomplete (in terms of context) and misleading. The OnSomething() is method from where?

Describe the solution you'd like

Questions are stated in the problem.

Additional context

No response

gragra33 commented 1 year ago

@htmlsplash I can think of a number of ways of doing this but it depends on the context, what do you want to share or do exactly?

htmlsplash commented 1 year ago

If you look at example 1 again: I would like to pass a reference of ComponentA to ComponentB (via declared parameter "ComponentA" in ComponentB) so that Component B internally can use the reference (to ComponentA) to initialize and render itself correctly.

A more clearer example:

SiteMapComponent Menu SiteMap="SiteMapComponent"

NOTE: I am omitting greater than/less than from the example because the text gets filtered out.

gragra33 commented 1 year ago

@htmlsplash So you have shared metadata and you have control over the code of both components?

2 methods come to mind:

  1. Inject a model/service into both that contains/manages the metadata
  2. have a property on both components that share a metadata model class

There are other ways, however, these are common methods.

htmlsplash commented 1 year ago

Thank you for your feedback.

I would not call this metadata, more like abstractions and delegation of concerns between components.

Unfortunately both proposed solutions are not really good solutions to the problem but just workarounds that require a lot of work to migrate over legacy webforms code to Blazor.

Issue with solution 1: Have to create extra api/change existing design to accommodate the scenario. My DI will explode with services if I just take that approach. All I want is single scenario where one UI component references another. This seems so intuitive in component based design framework (which I thought Blazor is).

Issue with solution 2:

gragra33 commented 1 year ago

I would not call this metadata, more like abstractions and delegation of concerns between components. Issue with solution 1: Have to create extra api/change existing design to accommodate the scenario. My DI will explode with services if I just take that approach. All I want is single scenario where one UI component references another.

Injecting a service class would be the common method. All common code is contained in the service class, then the components handle their unique bits.

This seems so intuitive in component based design framework (which I thought Blazor is).

Blazor is component-based, but not in the manner that you are asking for. You will see many warnings from the IDE and compiler, but it is possible, to a degree.

Issue with solution 2: Blazor encourages you to pass data via Parameter properties. I want to stick with this instead of sprinkling my code all over the life cycle methods to set properties. I want this to be done declaratively.

If you want to use parameters, then look at Cascading Parameters.

A great resouce to delve further into this, is the Blazor University.

htmlsplash commented 1 year ago

"Injecting a service class would be the common method. All common code is contained in the service class, then the components handle their unique bits." - I know that, ... But I feel that for this use case (and others) where one component references another component (like Menu referencing a sitemap data source) in just one place/location is overkill to create a service.

You have conveniently skipped over the part that work/effort required to start moving all this code and re-designing the entire UI model.

In summary: The proposed solution for anyone coming from UI framework like Webforms and being able to have one component reference another is show stopper to even think about conversion.

Q: Is there a technical reason why I cannot reference one component from another? Please also address the 2nd question why HTML elements have valid references outside onAfterRender method? Why is there such inconsistency and it does create confusion.

gragra33 commented 1 year ago

Q: Is there a technical reason why I cannot reference one component from another?

It depends on what exactly you are trying to achieve. And yes, I have looked at your generic sample.

Can you give a more concrete real-world example?

htmlsplash commented 1 year ago

Let me clarify:

I would like to have a menu component reference the SiteMapDataSource component via "SiteMapDataSource" parameter defined in the menu so that when the Menu.OnParametersSet() life cycle method executes, it has valid reference to SiteMapDataSource component (instead of null reference).

gragra33 commented 1 year ago

The SiteMapDataSource you have defined as a data source, so then, that is a child-parent relationship. The MenuComponent is dependent on the SiteMapDataSource.

For this scenario, then it would be something like:

<MenuComponent>
    <SiteMapDataSource>
    </SiteMapDataSource>
</MenuComponent>

The MenuComponent would then have CascadingValue to pass itself down to the SiteMapDataSource.

<CascadingValue Value="this">
    @this.ChildContent
</CascadingValue>
htmlsplash commented 1 year ago

Looks more elegant and what I am after but:

Clarification: It is the Menu that references the SiteMapDataSource, not the other way around. The SiteMapDataSource doesn't know anything about the Menu, but the Menu knows about the SiteMapDataSource, because it is the consumer of the SiteMap.

Would I just flip everything around in your example? SiteMapDataSource MenuComponent MenuComponent SiteMapDataSource

gragra33 commented 1 year ago

It's up to you, but the convention is to set the data source to the object that requires it.

Here is an example:

<DataList Title="Categories" 
          TItem="SearchCategory"
          AlternateRowCount=2
          VirtualItemSize=50
          VirtualOverscanCount=5
          EnableVirtualization=true
          @bind-SelectedItem=SearchContainer1.SelectedItem>
    <CategoryDataProvider
        SearchText=@SearchContainer1.SearchText
        LeafOnly=@SearchContainer1.LeafOnly>
    </CategoryDataProvider>
    <DataListTemplates TItem="SearchCategory">
        <ItemTemplate Context="SearchCategory">
            <div class="c-item__id">@SearchCategory.Id</div>
            <div class="c-item__name">@SearchCategory.Name</div>
        </ItemTemplate>
        <VirtualPlaceholder>
            <div class="c-row-loader">
                loading...
            </div>
        </VirtualPlaceholder>
    </DataListTemplates>
</DataList>

I am passing external parameters to the CategoryDataProvider outside of the DataList and the DataList is talking to the CategoryDataProvider to dynamically load the data.

htmlsplash commented 1 year ago

Is there a complete example on git? If yes, link please.

gragra33 commented 1 year ago

The sample above is a cut-down version from a commercial project. If you look at the link provided above, all that you require is there.

htmlsplash commented 1 year ago

I only see Blazor University link, is that the link you are referring to?

htmlsplash commented 1 year ago

Okay. I will give this a try in the upcoming days ... super late here, going to bed. Thanks. I still would like to know from someone why HTML elements get a preferential treatment.

gragra33 commented 1 year ago

Clarification: It is the Menu that references the SiteMapDataSource, not the other way around.

To address this, an example could be a Calendar Component. The data source would be accessed through the parent, and the Day, Week, Month, Year, etc views would be the child components. The data source could be a source component (like the DataList) above, of the parent if different data sources could be used.

Blazor's EditForm component is the data source for the input components.

It all depends on the requirements and the best-suited design to meet the requirements.

htmlsplash commented 1 year ago

In the Webforms world, the typical flow was: Data-Bound Control always referenced Data-Source Control

Therefore, any data bound Control required a reference to some data source in the form of: Control (via DataSourceID) or directly via binding a collection of items (via DataSource property, DataBind() method)

Not much flexibility, I am trying to retain the same design or as close to which allows easier/quicker conversion (and want to minimize changing the design in order to reduce confusion).

gragra33 commented 1 year ago

A DataSource, by its nature, provides access to data for a parent that does something with the data. How you implement it, is up to you.

Here is a commercial 3rd-party library provider and how they do it: SyncFusion - ListBox Data Binding

htmlsplash commented 1 year ago

Yes. I am referring to which control know about other control (in a consumer/producer pattern). In SyncFusion's ListBox example the ListBox references the data collection, but the data collection doesn't know anything about the Data-Bound controls. One way relationship. The implementation is still dependent on which way you model the dependency flow.

gragra33 commented 1 year ago

Yes, as I wrote:

The SiteMapDataSource you have defined as a data source, so then, that is a child-parent relationship. The MenuComponent is dependent on the SiteMapDataSource.

I will try and put a working sample together in the next day or so.

htmlsplash commented 1 year ago

Thanks. That would be helpful.

gragra33 commented 1 year ago

@htmlsplash As promised, here is an example based on your parameters: BlazorDataConsumer.

If you look at Blazor University Creating a TabControl you will see the same Parent/Child relationship. The difference with my sample code is that I'm using interfaces to reduce the tight coupling. This allows any parent component to use any child data source component. To reduce coupling further, you would make the model a generic base class or interface.

htmlsplash commented 1 year ago

Looks Good. Please leave this on for reference and others. I will tweak it for my needs. A+ for the effort.

gragra33 commented 1 year ago

Glad to hear. As you saw, I added a no-DataSourceComponent support to better illustrate what we were discussing.

Lastly, you should be able to see how to expand on this to add more child-parent relationships, like Settings, or Template support.

Enjoy.

javiercn commented 1 year ago

Is there a technical reason why I cannot reference one component from another?

Yes, components are created as part of the render process, not outside of it, which means a reference to a component can't be captured until a render has happened.

Please also address the 2nd question why HTML elements have valid references outside onAfterRender method? Why is there such inconsistency and it does create confusion.

There isn't. HtmlElement references are just a "pointer/name" that we give an HTML element when we are going to render it on to the page. That reference is only valid once the element has been rendered on to the page (hence why you only can access it safely during OnAfterRender).

It might be available at other times in other lifecycle methods (like on successive calls to SetParametersAsync), but only OnAfterRender guarantees whether or not it is available and whether or not it will be up to date.

The ways to do share state across components are either via a service, or a cascading value common to both locations.

ghost commented 1 year ago

This issue has been resolved and has not had any activity for 1 day. It will be closed for housekeeping purposes.

See our Issue Management Policies for more information.