From b8637db2be3b4956227a161fb1f28b10b70f2c53 Mon Sep 17 00:00:00 2001 From: YokeshSF4393 Date: Thu, 3 Sep 2026 13:29:22 +0530 Subject: [PATCH 1/2] 1052437: Fixed blazor toolkit chart rendering issue in SSR --- .../Chart/Renderer/ChartElementRenderer.cs | 12 ++++++ .../BaseRenderers/SeriesContainer.cs | 35 ++++++++++++++-- .../MarkerRenders/ChartMarkerRenderer.cs | 27 ++++++++++++- .../ChartStriplineContainer.cs | 19 ++++----- .../ChartStriplineRenderer.cs | 12 ++++++ .../Charts/Chart/Series/Marker/ChartMarker.cs | 40 +++++++++++++++++-- src/Components/Charts/Chart/SfChart.razor.cs | 18 ++++++--- .../Charts/Common/ChartUtils/ChartHelper.cs | 8 ++++ 8 files changed, 147 insertions(+), 24 deletions(-) diff --git a/src/Components/Charts/Chart/Renderer/ChartElementRenderer.cs b/src/Components/Charts/Chart/Renderer/ChartElementRenderer.cs index 08e0c3f..696eea3 100644 --- a/src/Components/Charts/Chart/Renderer/ChartElementRenderer.cs +++ b/src/Components/Charts/Chart/Renderer/ChartElementRenderer.cs @@ -128,6 +128,11 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) return; } + if (IsStaticSSR()) + { + SetDefaultRendererValues(); + } + this.CreateCascadingValue(builder, 0, 1, this, 2, BuildRenderers); RendererShouldRender = false; @@ -203,6 +208,13 @@ internal void SetDefaultRendererContainerValues() internal void Prerender() { ContainerUpdate = true; + + if (IsStaticSSR()) + { + ContainerPrerender = true; + RendererShouldRender = true; + SetDefaultRendererValues(); + } _ = InvokeAsync(StateHasChanged); } #endregion diff --git a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs index def1817..a67ff82 100644 --- a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs +++ b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs @@ -371,11 +371,15 @@ private void CreateSeriesElements(RenderTreeBuilder builder, IChartElement eleme /// /// Creates nested renderer components for a given series element, such as marker renderer, data label renderer, error bar renderer, and gradient renderers, based on the presence of their respective RendererType properties. Each nested renderer is also assigned a unique key for Blazor's rendering system. This method ensures that all auxiliary renderers associated with a series are instantiated and linked to the series for proper rendering of markers, labels, error bars, and gradients as needed. /// - private static void CreateSeriesNestedElements(RenderTreeBuilder builder, ChartSeries element) + private static void CreateSeriesNestedElements(RenderTreeBuilder builder, ChartSeries element, SfChart Chart) { int seq = 0; ChartSeries series = element; - if (series.Marker?.RendererType is not null) + bool isStaticSsr = Chart != null && Chart.IsStaticServerRendering(); + + bool shouldOpenMarker = series.Marker?.RendererType is not null && + (series.Marker.Visible || series.Marker.DataLabel.Visible || isStaticSsr); + if (shouldOpenMarker) { builder.OpenComponent(seq++, series.Marker.RendererType); builder.AddAttribute(seq++, "Series", series); @@ -706,7 +710,7 @@ protected override void BuildRenderers(RenderTreeBuilder builder) foreach (ChartSeries element in Elements.Cast()) { - CreateSeriesNestedElements(builder, element); + CreateSeriesNestedElements(builder, element, Owner ?? null!); } } else @@ -736,7 +740,7 @@ protected override void BuildRenderers(RenderTreeBuilder builder) continue; } - CreateSeriesNestedElements(builder, element); + CreateSeriesNestedElements(builder, element, Owner ?? null!); } } } @@ -972,6 +976,16 @@ internal override void SetDefaultRendererValues() ProcessData(); Owner?._legendRenderer?.SetDefaultRendererValues(); Owner?._axisContainer?.SetDefaultRendererContainerValues(); + if (Owner?.InitialRect is not null) + { + HandleChartSizeChange(Owner.InitialRect); + foreach (var seriesRenderer in Renderers.OfType()) + { + seriesRenderer.Series?.Marker?.Renderer?.HandleChartSizeChange(Owner.InitialRect); + } + Owner?._striplineBehindContainer?.SetDefaultRendererValues(); + Owner?._striplineOverContainer?.SetDefaultRendererValues(); + } } catch { @@ -1099,6 +1113,19 @@ public override void HandleChartSizeChange(Rect rect) { renderer.HandleChartSizeChange(rect); } + + if (IsStaticSSR()) + { + // Markers again if they were null during series pass + foreach (var seriesRenderer in Renderers.OfType()) + { + seriesRenderer.Series?.Marker?.Renderer?.HandleChartSizeChange(rect); + } + + // Striplines after axis + series clip are final + Owner?._striplineBehindContainer?.UpdateStriplineCollection(); + Owner?._striplineOverContainer?.UpdateStriplineCollection(); + } } /// diff --git a/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs b/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs index 36bc881..6b4c211 100644 --- a/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs +++ b/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs @@ -361,6 +361,12 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) } } + protected override bool ShouldRender() + { + // Always allow the first SSR pass + return RendererShouldRender || IsStaticSSR(); + } + #endregion #region Internal Methods @@ -377,7 +383,7 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) /// Calculated SymbolOptions instance. internal static SymbolOptions CalculateSymbol(ChartEventLocation location, string shape, Size size, string url, PathOptions option, SfChart chart) { - ChartEventLocation currentLocation = null!; + ChartEventLocation currentLocation = chart.IsStaticServerRendering() ? location : null!; if (chart is not null && shape == "Circle") { string[] locations = ChartHelper.AppendTextElements(chart, option.Id, location.X, location.Y, "cx", "cy"); @@ -388,7 +394,10 @@ internal static SymbolOptions CalculateSymbol(ChartEventLocation location, strin if (shapeoption.ShapeName == ShapeName.Path) { - shapeoption.PathOption.Direction = ChartHelper.AppendPathElements(chart ?? null!, shapeoption.PathOption.Direction, shapeoption.PathOption.Id); + if (chart != null) + { + shapeoption.PathOption.Direction = ChartHelper.AppendPathElements(chart, shapeoption.PathOption.Direction, shapeoption.PathOption.Id); + } shapeoption.PathOption.Visibility = option.Visibility; } @@ -506,6 +515,20 @@ internal void UpdateMarkerBorderWidth() } } + /// + /// SSR entry point – computes marker symbols when OnAfterRenderAsync is unavailable. + /// + internal override void SetDefaultRendererValues() + { + SeriesRenderer = Series?.Renderer; + if (SeriesRenderer != null && (Series?.Marker?.Visible ?? false)) + { + CalculateRenderTreeBuilderOptions(); // fills _symbolOptions + SeriesRenderer.CalculateMarkerClipPath(); + } + RendererShouldRender = true; + } + #endregion #region Public Methods diff --git a/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs b/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs index 0700b03..b0343d7 100644 --- a/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs +++ b/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs @@ -65,7 +65,9 @@ internal override void SetDefaultRendererValues() { ClipRect = Owner._axisContainer.AxisLayout.SeriesClipRect ?? null!; } + HandleChartSizeChange(Owner?.InitialRect ?? new Rect(0, 0, 0, 0)); + RendererShouldRender = true; } #endregion @@ -149,15 +151,14 @@ protected override void OnInitialized() /// RenderTreeBuilder instance. protected override void BuildRenderers(RenderTreeBuilder builder) { - if (builder is null) - { - return; - } - if (IsStaticSSR()) { SetDefaultRendererValues(); } + if (builder is null) + { + return; + } Sequence = 0; string id = Owner?.ID + "_stripline_" + "Behind" + "_"; @@ -221,14 +222,14 @@ protected override void OnInitialized() /// RenderTreeBuilder instance. protected override void BuildRenderers(RenderTreeBuilder builder) { - if (builder is null) + if (IsStaticSSR()) { - return; + SetDefaultRendererValues(); } - if (IsStaticSSR()) + if (builder is null) { - SetDefaultRendererValues(); + return; } Sequence = 0; diff --git a/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineRenderer.cs b/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineRenderer.cs index 76fc6af..5c30a2b 100644 --- a/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineRenderer.cs +++ b/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineRenderer.cs @@ -643,6 +643,18 @@ internal void UpdateCustomization(string property) } } + /// + /// SSR entry point – calculates stripline geometry when OnAfterRenderAsync is unavailable. + /// + internal override void SetDefaultRendererValues() + { + if (Stripline != null) + { + InitStripline(); // this is the method that already exists and fills the path/rect collections + } + RendererShouldRender = true; + } + #endregion #region Public Methods diff --git a/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs b/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs index 224adfa..f9f3d3f 100644 --- a/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs +++ b/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs @@ -1,6 +1,7 @@ -using System.ComponentModel; -using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; +using Syncfusion.Blazor.Toolkit.Charts.Internal; +using System.ComponentModel; namespace Syncfusion.Blazor.Toolkit.Charts { @@ -99,9 +100,40 @@ private void UpdateLegend() /// private void SetDefaultRendererValues() { - if (Series?.Container is not null && Series.Renderer is not null && Series.Renderer.IsStaticSSR()) + // Guard everything – any of these can still be null on the first SSR pass + if (Series == null || Series.Container == null || Series.Renderer == null) { - Series.Container._seriesContainer?.Prerender(); + return; + } + + if (!Series.Renderer.IsStaticSSR()) + { + return; + } + + // 1. Force the series container to run its layout / prerender + Series.Container._seriesContainer?.Prerender(); + + // 2. If the marker renderer component already exists, force it to calculate symbols + var markerRenderer = Series.Marker?.Renderer; + if (markerRenderer != null) + { + var rect = Series.Container.InitialRect ?? new Rect(0, 0, 0, 0); + markerRenderer.HandleChartSizeChange(rect); + } + else + { + // Renderer component has not been created yet. + // Just ask the series renderer to re-render; the OpenComponent will happen later. + try + { + Series.Renderer.RendererShouldRender = true; + Series.Renderer.ProcessRenderQueue(); + } + catch + { + // Ignore – under pure SSR ProcessRenderQueue may not be fully ready + } } } diff --git a/src/Components/Charts/Chart/SfChart.razor.cs b/src/Components/Charts/Chart/SfChart.razor.cs index 4f8d94b..6eaef5a 100644 --- a/src/Components/Charts/Chart/SfChart.razor.cs +++ b/src/Components/Charts/Chart/SfChart.razor.cs @@ -588,7 +588,7 @@ private double GetInitialCoordinate(double adaptiveMargin, double staticMargin, private void CalculateAvailableSize() { double height = Height != NullDimensionValue ? ChartHelper.StringToNumber(Height, _elementOffset.Height) : ChartDefaultHeight; - double width = Width != NullDimensionValue ? ChartHelper.StringToNumber(Width, _elementOffset.Width) : ChartDefaultHeight; + double width = Width != NullDimensionValue ? ChartHelper.StringToNumber(Width, _elementOffset.Width) : ChartDefaultWidth; AvailableSize = new Size(width > 0 ? width : AvailableSize.Width, height > 0 ? height : AvailableSize.Height); if (EnableAdaptiveRendering) @@ -1718,16 +1718,24 @@ internal void InitializeStaticChart() { if (!_render.IsSizeSet) { + // Force defaults when no DOM measurement is possible + if (_elementOffset.Width <= 0 || _elementOffset.Height <= 0) + { + _svgWidth ??= ChartDefaultWidth.ToString(CultureInfo.InvariantCulture); + _svgHeight ??= ChartDefaultHeight.ToString(CultureInfo.InvariantCulture); + } + CalculateAvailableSize(); SetInitialRect(); + + // Critical for SSR – process data & layout synchronously + ProcessData(); + Prerender(); // series / axis / column / row / annotation } } catch { - if (!IsDisposed) - { - throw; - } + if (!IsDisposed) throw; } } diff --git a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs index 78f7518..215afd9 100644 --- a/src/Components/Charts/Common/ChartUtils/ChartHelper.cs +++ b/src/Components/Charts/Common/ChartUtils/ChartHelper.cs @@ -1215,6 +1215,14 @@ internal static string AppendPathElements(SfChart chart, string direction, strin /// An array containing the string representation of X and Y coordinates. internal static string[] AppendTextElements(SfChart chart, string id, double locationX, double locationY, string x = "x", string y = "y") { + if (chart == null && chart.IsStaticServerRendering()) + { + return new string[] + { + locationX.ToString(CultureInfo.InvariantCulture), + locationY.ToString(CultureInfo.InvariantCulture) + }; + } bool redraw = chart._redraw; if (chart._textAnimationElements.TryGetValue(id, out DynamicTextAnimationOptions? existElement)) From f5c9a895c2b912d0a8501a6c7e3c1f9b02e3be0c Mon Sep 17 00:00:00 2001 From: YokeshSF4393 Date: Thu, 3 Sep 2026 16:37:20 +0530 Subject: [PATCH 2/2] 1052437: Addressed review changes for the SSR issue. --- .../BaseRenderers/SeriesContainer.cs | 16 ++++++---------- .../MarkerRenders/ChartMarkerRenderer.cs | 10 ++++++++-- .../ChartStriplineContainer.cs | 17 +++++++++-------- .../Charts/Chart/Series/Marker/ChartMarker.cs | 16 +--------------- src/Components/Charts/Chart/SfChart.razor.cs | 10 ++++++---- 5 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs index a67ff82..3edd417 100644 --- a/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs +++ b/src/Components/Charts/Chart/Renderer/SeriesRenderers/BaseRenderers/SeriesContainer.cs @@ -371,14 +371,13 @@ private void CreateSeriesElements(RenderTreeBuilder builder, IChartElement eleme /// /// Creates nested renderer components for a given series element, such as marker renderer, data label renderer, error bar renderer, and gradient renderers, based on the presence of their respective RendererType properties. Each nested renderer is also assigned a unique key for Blazor's rendering system. This method ensures that all auxiliary renderers associated with a series are instantiated and linked to the series for proper rendering of markers, labels, error bars, and gradients as needed. /// - private static void CreateSeriesNestedElements(RenderTreeBuilder builder, ChartSeries element, SfChart Chart) + private static void CreateSeriesNestedElements(RenderTreeBuilder builder, ChartSeries element) { int seq = 0; ChartSeries series = element; - bool isStaticSsr = Chart != null && Chart.IsStaticServerRendering(); bool shouldOpenMarker = series.Marker?.RendererType is not null && - (series.Marker.Visible || series.Marker.DataLabel.Visible || isStaticSsr); + (series.Marker.Visible || series.Marker.DataLabel.Visible); if (shouldOpenMarker) { builder.OpenComponent(seq++, series.Marker.RendererType); @@ -710,7 +709,7 @@ protected override void BuildRenderers(RenderTreeBuilder builder) foreach (ChartSeries element in Elements.Cast()) { - CreateSeriesNestedElements(builder, element, Owner ?? null!); + CreateSeriesNestedElements(builder, element); } } else @@ -740,7 +739,7 @@ protected override void BuildRenderers(RenderTreeBuilder builder) continue; } - CreateSeriesNestedElements(builder, element, Owner ?? null!); + CreateSeriesNestedElements(builder, element); } } } @@ -987,12 +986,9 @@ internal override void SetDefaultRendererValues() Owner?._striplineOverContainer?.SetDefaultRendererValues(); } } - catch + catch (Exception exception) when (IsDisposed) { - if (!IsDisposed) - { - throw; - } + System.Diagnostics.Debug.WriteLine($"Chart series renderer initialization failed during disposal: {exception}"); } } diff --git a/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs b/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs index 6b4c211..d374836 100644 --- a/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs +++ b/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs @@ -13,6 +13,8 @@ internal class ChartMarkerRenderer : ChartRenderer, IChartElementRenderer #region Fields private string? _seriesIndex; private List _symbolOptions = []; + // Static SSR is allowed once; subsequent passes use the normal renderer gate. + private bool _staticSSRRenderPending = true; #endregion #region Properties @@ -359,12 +361,16 @@ protected override void BuildRenderTree(RenderTreeBuilder builder) RenderMarkers(builder); builder.CloseElement(); } + + if (IsStaticSSR()) + { + _staticSSRRenderPending = false; + } } protected override bool ShouldRender() { - // Always allow the first SSR pass - return RendererShouldRender || IsStaticSSR(); + return RendererShouldRender || (IsStaticSSR() && _staticSSRRenderPending); } #endregion diff --git a/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs b/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs index b0343d7..7629e5f 100644 --- a/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs +++ b/src/Components/Charts/Chart/Renderer/StriplineRenderer/ChartStriplineContainer.cs @@ -151,15 +151,16 @@ protected override void OnInitialized() /// RenderTreeBuilder instance. protected override void BuildRenderers(RenderTreeBuilder builder) { - if (IsStaticSSR()) - { - SetDefaultRendererValues(); - } if (builder is null) { return; } + if (IsStaticSSR()) + { + SetDefaultRendererValues(); + } + Sequence = 0; string id = Owner?.ID + "_stripline_" + "Behind" + "_"; double width = ClipRect.Width > 0 ? ClipRect.Width : 0, @@ -222,14 +223,14 @@ protected override void OnInitialized() /// RenderTreeBuilder instance. protected override void BuildRenderers(RenderTreeBuilder builder) { - if (IsStaticSSR()) + if (builder is null) { - SetDefaultRendererValues(); + return; } - if (builder is null) + if (IsStaticSSR()) { - return; + SetDefaultRendererValues(); } Sequence = 0; diff --git a/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs b/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs index f9f3d3f..9ffb452 100644 --- a/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs +++ b/src/Components/Charts/Chart/Series/Marker/ChartMarker.cs @@ -114,27 +114,13 @@ private void SetDefaultRendererValues() // 1. Force the series container to run its layout / prerender Series.Container._seriesContainer?.Prerender(); - // 2. If the marker renderer component already exists, force it to calculate symbols + // Recalculate symbols when the marker renderer component already exists. var markerRenderer = Series.Marker?.Renderer; if (markerRenderer != null) { var rect = Series.Container.InitialRect ?? new Rect(0, 0, 0, 0); markerRenderer.HandleChartSizeChange(rect); } - else - { - // Renderer component has not been created yet. - // Just ask the series renderer to re-render; the OpenComponent will happen later. - try - { - Series.Renderer.RendererShouldRender = true; - Series.Renderer.ProcessRenderQueue(); - } - catch - { - // Ignore – under pure SSR ProcessRenderQueue may not be fully ready - } - } } /// diff --git a/src/Components/Charts/Chart/SfChart.razor.cs b/src/Components/Charts/Chart/SfChart.razor.cs index 6eaef5a..d54030b 100644 --- a/src/Components/Charts/Chart/SfChart.razor.cs +++ b/src/Components/Charts/Chart/SfChart.razor.cs @@ -1728,14 +1728,16 @@ internal void InitializeStaticChart() CalculateAvailableSize(); SetInitialRect(); - // Critical for SSR – process data & layout synchronously + // Border initialization reaches this method before the normal layout pass; + // process data first so the synchronous prerender sees populated renderers. + // IsSizeSet keeps this SSR-only initialization from repeating. ProcessData(); - Prerender(); // series / axis / column / row / annotation + Prerender(); } } - catch + catch (Exception exception) when (IsDisposed) { - if (!IsDisposed) throw; + System.Diagnostics.Debug.WriteLine($"Static chart initialization failed during disposal: {exception}"); } }