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

[Blazor] Accessing parent component/setting contstraint on CascadingValue component #30500

Closed daniel-p-tech closed 3 years ago

daniel-p-tech commented 3 years ago

Hi,

I'm designing a custom component that serves as a container for direct child elements defined as a pair of <CustomLabel> and <CustomInput> components. The idea is to set the for attribute of the <CustomLabel> element without user explicitly specifying any component parameters. Please refer to the code snippet below.

EditFormItem.razor:

<CascadingValue Value="this" IsFixed="true">
    @ChildContent
</CascadingValue>

EditFormItem.razor.cs:

[Parameter]
public RenderFragment ChildContent { get; set; }

private CustomLabel m_label;
private CustomInput m_field;

internal void AddLabel(CustomLabel label)
{
    if (m_label != null)
    {
        ThrowDuplicateCopmonentException("Label");
    }
    m_label = label;
    UpdateLabelFor();
}

internal void AddField(CustomInput field)
{
    if (m_field != null)
    {
        ThrowDuplicateCopmonentException("Field");
    }
    m_field = field;
    UpdateLabelFor();
}

private void UpdateLabelFor()
{
    if (m_label != null && m_field != null)
    {
        m_label.For = m_field;
    }
}

CustomLabel.razor

@inherits BaseCustomComponent

<label for="@For?.Id">@Text</label>

CustomLabel.razor.cs

public partial class CustomLabel
{
    [Parameter]
    public string Text { get; set; }

    [CascadingParameter]
    public EditFormItem EditFormItem { get; set; }

    internal CustomInput For { get; set; }

    protected override void OnInitialized()
    {
        EditFormItem?.AddLabel(this);
    }
}

Usage:

<EditForm Model="@Film" OnValidSubmit="@SaveFilm">
  ...
  <EditFormItem>
      <CustomLabel Text="Release Date:" />
      <CustomDatePicker
          NullText="Select date..."
          MinDate="@DateTime.Parse("1/5/2000")"
          MaxDate="@DateTime.Parse("12/15/2021")"
          Width="20rem"/>
    </EditFormItem>
</EditForm>

This concept works fine for simple <CustomInput> components but breaks in case of my CustomDatePicker that contains other instances of my custom input components (such as a CustomComboBox for selected month and year). The problem is that the value of EditFormItem instance cascades to all descendants of EditFormItem component. I want the cascading to be limited only to direct descendants. I have read all documentation and various blog posts on Blazor but I don't see how I can set such constraint. Is this a deficiency of Blazor? If so, what is the workaround/other solution for the outlined problem?

In summary, I want direct child components to have access to the parent component without user setting any extra parameters. Any potential descendants of direct child components should not be included.

Thank you for your input.

ghost commented 3 years ago

Thanks for contacting us. We're moving this issue to the Next sprint planning milestone for future evaluation / consideration. We will evaluate the request when we are planning the work for the next milestone. To learn more about what to expect next and how this issue will be handled you can read more about our triage process here.

daniel-p-tech commented 3 years ago

Hi,

It has been close to a week with no response. I see that the issue was moved into "Discussions" and assigned to an engineer - can someone explain when I can expect an answer and what the purpose of "Discussions" is?

Thank you.

SteveSandersonMS commented 3 years ago

If you want a certain cascading parameter to be unavailable to descendants, you'll need to overwrite it with a null value for that particular subtree. For example, a component could receive a cascading parameter of type EditFormItem, and also prevent its own descendants from receiving it:

<h1>I have received @EditFormItem</h1>

@* Prevent child content from receiving the same cascading value for this type/name *@
<CascadingValue Value="default(EditFormItem)" IsFixed="true">
    @ChildContent
</CascadingValue>

@code {
    [CascadingParameter] public EditFormItem EditFormItem { get; set; }
    [Parameter] public RenderFragment ChildContent { get; set; }
}

We haven't had any other requests for a cascading value that only propagates to a certain depth in the hierarchy, so hopefully this solution will seem reasonable to you. If "cascading value that stops at the first receiver" becomes a common request, we'd certainly consider adding it in the future.

daniel-p-tech commented 3 years ago

Thank you @SteveSandersonMS for taking the time to respond to my issue. Your suggested solution does work, I wasn't aware of the fact that CascadingValue can be overwritten! Maybe something to state explicitly in the documentation?

On a related note, I ran into several scenarios when I had to declare CascadingValue just to acquire reference to the parent component. I read in one of the blogs that this may negatively affect performance in large applications. What is the rationale behind not being able to traverse component hierarchy like in other traditional frameworks? I think something as simple as exposing Parent property on ComponentBase class would be all I need.

Thanks for your help!

SteveSandersonMS commented 3 years ago

I read in one of the blogs that this may negatively affect performance in large applications

As long as you use IsFixed="true", the performance implications of a cascading parameter are almost indistinguishable from a regular non-cascading parameter. It's only more expensive if you don't say it's fixed, because then the framework has to track a set of subscriptions to issue notifications if the value changes.

I think something as simple as exposing Parent property on ComponentBase class would be all I need.

What we're planning to do in this area is better covered by https://github.com/dotnet/aspnetcore/issues/12302.

Relying on implicit parent/child relationships within the hierarchy tend to be very problematic in other frameworks (WebForms being a clear example), as people end up with code like ((MyAncestorComponent)this.Parent.Parent.Parent).DoSomething(), which is super fragile and makes it very hard to reason about what kinds of component hierarchies will work, and which will just crash at runtime.

With #12302, it would be possible to declare in a strongly-typed way what specific ancestors must exist, and it would be validated at compile-time, and the tooling could even guide the developer to produce only legal hierarchies via code completion hints.

That is some way further off in the future, but we'd rather not introduce a different and unsafe version of that in the short term, as that would have long-term costs. In the short term, using a <CascadingValue> with IsFixed="true" is hopefully a decent and not-too-inconvenient solution. It also avoids the Parent.Parent.Parent issue.