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.15k stars 9.92k forks source link

Using per page/component render modes, cascading parameters turn null after rendering. #53482

Open carlfranklin opened 7 months ago

carlfranklin commented 7 months ago

Is there an existing issue for this?

Describe the bug

Using per page/component render modes, cascading parameters turn null after rendering.

Expected Behavior

The issue is documented with code at https://github.com/carlfranklin/Blazor8Test

Using per page/component render modes, cascading parameters turn null after rendering.

I have a .NET 8 Blazor Web App with the Interactive Render Mode set to Server, and the Interactivity Location set to Global.

This is my version info:

Microsoft Visual Studio Professional 2022
Version 17.8.5
VisualStudio.17.Release/17.8.5+34511.84
Microsoft .NET Framework
Version 4.8.09037

Installed Version: Professional

ASP.NET and Web Tools   17.8.358.6298
ASP.NET and Web Tools

Azure App Service Tools v3.0.0   17.8.358.6298
Azure App Service Tools v3.0.0

Azure Functions and Web Jobs Tools   17.8.358.6298
Azure Functions and Web Jobs Tools

C# Tools   4.8.0-7.23572.1+7b75981cf3bd520b86ec4ed00ec156c8bc48e4eb
C# components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Common Azure Tools   1.10
Provides common services for use by Azure Mobile Services and Microsoft Azure Tools.

DevExpress Dashboard Extension   1.4
A Visual Studio extension that invokes the Dashboard Designer editor.

DevExpress Reporting Extension   1.4
A Visual Studio extension that invokes the Report Designer editor for report definition VSREPX files.

DevExpress Reporting Tools Extension   1.0
Extends Visual Studio with tools required for the Report Designer editor.

DevExpress VSDesigner NETFramework Package   1.0
A Visual Studio extension that invokes the Report and Dashboard designer editors.

DevExpress.DeploymentTool   1.0
A useful tool for deploying DevExpress assemblies.

DevExpress.Win.LayoutAssistant Extension   1.0
DevExpress.Win.LayoutAssistant Visual Studio Extension Detailed Info

Extensibility Message Bus   1.4.39 (main@e8108eb)
Provides common messaging-based MEF services for loosely coupled Visual Studio extension components communication and integration.

GitHub Copilot   1.149.0.0 (v1.149.0.0@9a0f75deb)
GitHub Copilot is an AI pair programmer that helps you write code faster and with less work.

GitHub Copilot Agent   1.149.0

Microsoft JVM Debugger   1.0
Provides support for connecting the Visual Studio debugger to JDWP compatible Java Virtual Machines

Mono Debugging for Visual Studio   17.8.17 (957fbed)
Support for debugging Mono processes with Visual Studio.

NuGet Package Manager   6.8.0
NuGet Package Manager in Visual Studio. For more information about NuGet, visit https://docs.nuget.org/

Razor (ASP.NET Core)   17.8.3.2405201+d135dd8d2ec1c2fbdee220e8656b308694e17a4b
Provides languages services for ASP.NET Core Razor.

SQL Server Data Tools   17.8.120.1
Microsoft SQL Server Data Tools

TypeScript Tools   17.0.20920.2001
TypeScript Tools for Microsoft Visual Studio

Visual Basic Tools   4.8.0-7.23572.1+7b75981cf3bd520b86ec4ed00ec156c8bc48e4eb
Visual Basic components used in the IDE. Depending on your project type and settings, a different version of the compiler may be used.

Visual F# Tools   17.8.0-beta.23475.2+10f956e631a1efc0f7f5e49c626c494cd32b1f50
Microsoft Visual F# Tools

Visual Studio IntelliCode   2.2
AI-assisted development for Visual Studio.

VisualStudio.DeviceLog   1.0
Information about my package

VisualStudio.Mac   1.0
Mac Extension for Visual Studio

VSPackage Extension   1.0
VSPackage Visual Studio Extension Detailed Info

Xamarin   17.8.0.157 (d17-8@8e82278)
Visual Studio extension to enable development for Xamarin.iOS and Xamarin.Android.

Xamarin Designer   17.8.3.6 (remotes/origin/d17-8@eccf46a291)
Visual Studio extension to enable Xamarin Designer tools in Visual Studio.

Xamarin.Android SDK   13.2.2.0 (d17-5/45b0e14)
Xamarin.Android Reference Assemblies and MSBuild support.
    Mono: d9a6e87
    Java.Interop: xamarin/java.interop/d17-5@149d70fe
    SQLite: xamarin/sqlite/3.40.1@68c69d8
    Xamarin.Android Tools: xamarin/xamarin-android-tools/d17-5@ca1552d

In App.razor, I have disabled pre-rendering, but it also fails with pre-rendering on.

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="Blazor8Test.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet @rendermode="@InteractiveServer" />
</head>

<body>
    <!-- Turn off pre-rendering -->
    <Routes @rendermode="@(new InteractiveServerRenderMode(false))" />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>

I have a component in the client project called CascadingAppState.razor:

<CascadingValue Value="this">
    @ChildContent
</CascadingValue>
using Microsoft.AspNetCore.Components;

namespace Blazor8Test.Client;

public partial class CascadingAppState : ComponentBase
{
    [Parameter]
    public RenderFragment ChildContent { get; set; }

    private int count = 0;
    public int Count
    {
        get => count;
        set
        {
            count = value;
            StateHasChanged();
        }
    }
}

I have modified Routes.razor as follows:

<CascadingAppState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
        </Found>
    </Router>
</CascadingAppState>

I have implemented CascadingAppState in Counter.razor:

@page "/counter"

<PageTitle>Counter</PageTitle>

<h1>Counter Render Mode @renderMode</h1>

<p role="status">Current count: @AppState.Count</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {

    private int currentCount = 0;
    private string renderMode = "SSR";

    [CascadingParameter]
    public CascadingAppState AppState { get; set; } = null;

    private void IncrementCount()
    {
        AppState.Count++;
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            renderMode = OperatingSystem.IsBrowser() ? "Wasm" : "Server";
            StateHasChanged();
        }
    }
}

Note that I am also showing the current render mode: SSR, Server, or WASM.

Behavior

Run the app and navigate to the Counter page. It works as advertised

image-20240119085526314

image-20240119085431751

Increment the counter, navigate to Home and back. The value persists because of the Cascading App State:

image-20240119085520380

image-20240119085526314

image-20240119085520380

Now remove the "Global" feature in App.razor:

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="/" />
    <link rel="stylesheet" href="bootstrap/bootstrap.min.css" />
    <link rel="stylesheet" href="app.css" />
    <link rel="stylesheet" href="Blazor8Test.styles.css" />
    <link rel="icon" type="image/png" href="favicon.png" />
    <HeadOutlet />
</head>

<body>
    <Routes />
    <script src="_framework/blazor.web.js"></script>
</body>

</html>

And add this line on line 2 of Counter.razor:

@rendermode InteractiveServer

Run it again

image-20240119085526314

Upon navigation, you get a Null Reference Exception on AppState:

image-20240119085842892

Steps To Reproduce

Follow the instructions I have laid out

Exceptions (if any)

No response

.NET Version

Version info is included in description

Anything else?

No response

javiercn commented 7 months ago

@carlfranklin thanks for reaching out.

This is likely because you are trying to pass the cascading parameters across serialization boundaries (which is not generally supported)

There's a bug tracking support for this type of thing here, however, in general the values will have to be serializable https://github.com/dotnet/aspnetcore/issues/51969

SQL-MisterMagoo commented 7 months ago

I think the problem here is that Routes.razor which contains the CascadingState is not marked as Interactive, so it does not participate in the interactivity.

If you mark Routes as Interactive, this problem goes away.

I guess you could also move the CascadingState to a new layout that is marked Interactive and have any Interactive routable pages use the new layout.

There may be other ways of course, but it doesn't seem like a bug that state parented in a non-interactive component is null.

carlfranklin commented 7 months ago

If you mark Routes as Interactive, isn't that effectively Global mode?

The goal is to have state accessible even when switching between Server and Wasm render modes, and even if you go to a SSR page and back.

It may not be a bug, but it means using per page/component mode (not Global) means giving up Cascading values, and therefore giving up state.

On Wed, Jan 31, 2024 at 6:57 AM SQL-MisterMagoo @.***> wrote:

I think the problem here is that Routes.razor which contains the CascadingState is not marked as Interactive, so it does not participate in the interactivity.

If you mark Routes as Interactive, this problem goes away.

I guess you could also move the CascadingState to a new layout that is marked Interactive and have any Interactive routable pages use the new layout.

There may be other ways of course, but it doesn't seem like a bug that state parented in a non-interactive component is null.

— Reply to this email directly, view it on GitHub https://github.com/dotnet/aspnetcore/issues/53482#issuecomment-1918963947, or unsubscribe https://github.com/notifications/unsubscribe-auth/AALK4DC46UQAF7FZ2GYOAPLYRIWSZAVCNFSM6AAAAABCCALPIWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSMJYHE3DGOJUG4 . You are receiving this because you were mentioned.Message ID: @.***>

SQL-MisterMagoo commented 7 months ago

Yes, making routes interactive is the same effectively as global, so you would push that down to a location where it is needed. If you don't want everything in routes to have access to the state, move the state down to where it is needed. If you do want everything in routes to have access to state then it needs to be interactive.

On Wed, 31 Jan 2024, 14:39 Carl Franklin, @.***> wrote:

If you mark Routes as Interactive, isn't that effectively Global mode?

The goal is to have state accessible even when switching between Server and Wasm render modes, and even if you go to a SSR page and back.

It may not be a bug, but it means using per page/component mode (not Global) means giving up Cascading values, and therefore giving up state.

On Wed, Jan 31, 2024 at 6:57 AM SQL-MisterMagoo @.***> wrote:

I think the problem here is that Routes.razor which contains the CascadingState is not marked as Interactive, so it does not participate in the interactivity.

If you mark Routes as Interactive, this problem goes away.

I guess you could also move the CascadingState to a new layout that is marked Interactive and have any Interactive routable pages use the new layout.

There may be other ways of course, but it doesn't seem like a bug that state parented in a non-interactive component is null.

— Reply to this email directly, view it on GitHub < https://github.com/dotnet/aspnetcore/issues/53482#issuecomment-1918963947>,

or unsubscribe < https://github.com/notifications/unsubscribe-auth/AALK4DC46UQAF7FZ2GYOAPLYRIWSZAVCNFSM6AAAAABCCALPIWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSMJYHE3DGOJUG4>

. You are receiving this because you were mentioned.Message ID: @.***>

— Reply to this email directly, view it on GitHub https://github.com/dotnet/aspnetcore/issues/53482#issuecomment-1919236750, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACWQFPVDVTXFHVWJMNJ6Z4DYRJJRTAVCNFSM6AAAAABCCALPIWVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSMJZGIZTMNZVGA . You are receiving this because you commented.Message ID: @.***>

sbwalker commented 7 months ago

There are a lot of limitations dealing with render mode boundaries... I documented a bunch of them here (including the CascadingParameter behavior):

https://www.linkedin.com/posts/shaunbrucewalker_blazor-dotnet8-activity-7128497980755603456-QsnX

mkArtakMSFT commented 6 months ago

Parking in Preview 7 to provide a sample demonstrating how to do this.

rockfordlhotka commented 6 months ago

@mkArtakMSFT hopefully the ultimate resolution allows bi-directional state management - which is to say that if I change a value in a wasm page, that value should continue to be consistent in server static and server interactive pages, and visa versa.

Basically, if I implement a Counter page and put the counter value in some cascading/global location, I should get the same Counter value in every render mode throughout the lifetime of a single app "session".

Otto404 commented 4 months ago

I think the confusion that MS created with cascading parameters in conjunction with static SSR and interactive server-side rendering was a major mistake. As far as I can see, the cascading parameters are now completely useless. I'm seriously considering ditching Blazor and using Flutter.

sbwalker commented 4 months ago

@Otto404 you may find the following example useful:

https://www.oqtane.org/blog/!/91/passing-state-to-components-using-ssr-in-net-8

It demonstrates a simple approach for using Cascading Parameters and Scoped Services in Blazor in .NET 8 when using static and interactive components. Note that it only works for the most common scenario I have seen in Blazor, where you are using Cascading Parameters and Scoped Services as essentially a "read-only immutable cache" for the current user session.

rockfordlhotka commented 4 months ago

I think the confusion that MS created with cascading parameters in conjunction with static SSR and interactive server-side rendering was a major mistake. As far as I can see, the cascading parameters are now completely useless. I'm seriously considering ditching Blazor and using Flutter.

Before leaving Blazor, I'd look at the solutions from myself and @sbwalker - I think we've come up with good answers to the challenge that make the new render modes work really well in a lot of scenarios. And the flexible render modes are (imo) very useful in real-world scenarios.

Otto404 commented 4 months ago

@rockfordlhotka could you give me a pointer to your solution? thanks,

rockfordlhotka commented 4 months ago

@Otto404

@rockfordlhotka could you give me a pointer to your solution? thanks,

I solved the issue without cascading parameters, instead basically creating a per-user "session" concept that is read-write across static, server-interactive, and wasm-interactive pages.

https://blog.lhotka.net/2023/11/28/Per-User-Blazor-8-State

richardaubin commented 1 month ago

@Otto404 you may find the following example useful:

https://github.com/sbwalker/Oqtane.State

It demonstrates a simple approach for using Cascading Parameters and Scoped Services in Blazor in .NET 8 when using static and interactive components. Note that it only works for the most common scenario I have seen in Blazor, where you are using Cascading Parameters and Scoped Services as essentially a "read-only immutable cache" for the current user session.

I've successfully implemented this approach and love it. It's super simple to setup and requires no additional dependencies.

sbwalker commented 1 month ago

@richardaubin I am glad you found the approach to be useful... it relies on the standard Blazor primitives to move state across render mode boundaries. However there is one caveat that I ran into recently which I should mention...

When Blazor passes parameters from a static component to an interactive component, it uses serialization. This is mentioned in the official documentation: https://learn.microsoft.com/en-us/aspnet/core/blazor/components/cascading-values-and-parameters?view=aspnetcore-8.0#cascading-valuesparameters-and-render-mode-boundaries. However it does not explain what actually happens under the covers.

The reality is that Blazor serializes and encrypts the parameters and injects them into the page output, where it then decrypts and deserializes them. Basically this is similar to "ViewState" and you can view the element in your page output ie.

<!--Blazor:{"type":"server","key":{"locationHash":"8FA5FA034079DC81B5EF9C4AE6EE08D8845F2721938D620E5FBD646F9273DC08:5","formattedComponentKey":""},"sequence":0,"descriptor":"CfDJ8JwRBrGNvOJEo4T2ARppP7N0umT9coWO/IGL8lA1QlaQDJER8="}-->

Note that serialization and encryption are NOT very efficient so you have to be aware of the size of the objects you are passing as parameters, or you may face some performance issues.

Recently I was investigating a Blazor performance issue and noticed that the response size of a page request was exceptionally large:

image

It turned out I was passing a collection as a parameter from a static component to an interactive component. The collection contained 200 fairly complex objects. After serialization/encryption it added almost 4 MB of content to the page!

It turns out that the default serialization/encryption approach in Blazor can handle parameters up to ~100KB in size fairly well... but once you go beyond that, you need to do some performance tuning. In my case the solution was to trim the state in the collection to include only the essential properties. Another potential solution would be to use a service to store the state elsewhere (ie. on the server in a cache if using Blazor Server, or in local storage if using WebAssembly)... but that obviously requires a lot more infrastructure and complexity (ie. see the solution above from @rockfordlhotka ).

richardaubin commented 1 month ago

@sbwalker Thanks for the update and extra information on this method.

Currently my use case is to populate the options of an ecommerce product details page, where the options is an interactive component allowing the product details page to update with a new product when the user selects a variant option. So I'm passing a view model for the component only, hence it's very lightweight.

I also experienced an unrelated issue that caused me to restructure my code a bit to make this work.

The objects I was serializing had a property with a base class type. The property values were derived type instances.

class ProductOptions
{
  public OptionBase Option { get; set; }
}

class ImageSwatchOption : OptionBase
{
  public string ImageSource { get; set; }
}

new ProductOptions 
{
  Option = new ImageSwatchOtpion();
}

When the dynamic component pre-rendered everything passed. But on the second render, the properties deserialized to the base type, which caused downstream logic to fail because the base type didn't contain the properties of the derived types. In the code example above, the ImageSource property of the ImageSwatchOption is discarded.

This analysis leads me to wonder if disabling pre-rendering on the interactive component is possible and if it would resolve the issue.

sbwalker commented 1 month ago

@richardaubin I ran into that scenario as well. The way I solved it was to create a custom DTO class which I populated with the required state values to transfer across the render mode boundary. The approach is also helpful if you need to trim the state to a smaller size for optimizing performance.