@@ -16,10 +16,14 @@ internal sealed class RenderedComponent<TComponent> : ComponentState, IRenderedC
1616
1717 [ SuppressMessage ( "Usage" , "CA2213:Disposable fields should be disposed" , Justification = "Owned by BunitServiceProvider, disposed by it." ) ]
1818 private readonly BunitHtmlParser htmlParser ;
19-
19+ private int renderCount ;
2020 private string markup = string . Empty ;
21+ private int markupStartIndex ;
22+ private int markupEndIndex ;
2123 private INodeList ? latestRenderNodes ;
2224
25+ public bool IsDirty { get ; set ; }
26+
2327 /// <summary>
2428 /// Gets the component under test.
2529 /// </summary>
@@ -53,10 +57,22 @@ public string Markup
5357 }
5458 }
5559
60+ /// <summary>
61+ /// Adds or removes an event handler that will be triggered after
62+ /// each render of this <see cref="RenderedComponent{T}"/>.
63+ /// </summary>
64+ public event EventHandler ? OnAfterRender ;
65+
66+ /// <summary>
67+ /// An event that is raised after the markup of the
68+ /// <see cref="RenderedComponent{T}"/> is updated.
69+ /// </summary>
70+ public event EventHandler ? OnMarkupUpdated ;
71+
5672 /// <summary>
5773 /// Gets the total number times the fragment has been through its render life-cycle.
5874 /// </summary>
59- public int RenderCount { get ; private set ; }
75+ public int RenderCount => renderCount ;
6076
6177 /// <summary>
6278 /// Gets the AngleSharp <see cref="INodeList"/> based
@@ -77,6 +93,10 @@ public INodeList Nodes
7793 /// </summary>
7894 public IServiceProvider Services { get ; }
7995
96+ int IRenderedComponent . RenderCount { get => renderCount ; set { renderCount = value ; } }
97+
98+ public IRenderedComponent ? Root { get ; }
99+
80100 public RenderedComponent (
81101 BunitRenderer renderer ,
82102 int componentId ,
@@ -89,57 +109,76 @@ public RenderedComponent(
89109 this . renderer = renderer ;
90110 this . instance = ( TComponent ) instance ;
91111 htmlParser = Services . GetRequiredService < BunitHtmlParser > ( ) ;
112+ var parentRenderedComponent = parentComponentState as IRenderedComponent ;
113+ Root = parentRenderedComponent ? . Root ?? parentRenderedComponent ;
92114 }
93115
94- /// <summary>
95- /// Adds or removes an event handler that will be triggered after each render of this <see cref="RenderedComponent{T}"/>.
96- /// </summary>
97- public event EventHandler ? OnAfterRender ;
116+ /// <inheritdoc/>
117+ public void Dispose ( )
118+ {
119+ if ( IsDisposed )
120+ return ;
98121
99- /// <summary>
100- /// An event that is raised after the markup of the <see cref="RenderedComponent{T}"/> is updated.
101- /// </summary>
102- public event EventHandler ? OnMarkupUpdated ;
122+ if ( Root is not null )
123+ Root . IsDirty = true ;
124+
125+ IsDisposed = true ;
126+ markup = string . Empty ;
127+ OnAfterRender = null ;
128+ OnMarkupUpdated = null ;
129+ }
130+
131+ /// <inheritdoc/>
132+ public override ValueTask DisposeAsync ( )
133+ {
134+ Dispose ( ) ;
135+ return base . DisposeAsync ( ) ;
136+ }
137+
138+ public void SetMarkupIndices ( int start , int end )
139+ {
140+ markupStartIndex = start ;
141+ markupEndIndex = end ;
142+ IsDirty = true ;
143+ }
103144
104145 /// <summary>
105146 /// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
106147 /// </summary>
107- public void UpdateState ( bool hasRendered , bool isMarkupGenerationRequired )
148+ public void UpdateMarkup ( )
108149 {
109150 if ( IsDisposed )
110151 return ;
111152
112- if ( hasRendered )
153+ if ( Root is RenderedComponent < BunitRootComponent > root )
113154 {
114- RenderCount ++ ;
155+ var newMarkup = root . markup [ markupStartIndex ..markupEndIndex ] ;
156+ if ( markup != newMarkup )
157+ {
158+ Volatile . Write ( ref markup , newMarkup ) ;
159+ latestRenderNodes = null ;
160+ OnMarkupUpdated ? . Invoke ( this , EventArgs . Empty ) ;
161+ }
162+ else
163+ {
164+ // no change
165+ }
115166 }
116-
117- if ( isMarkupGenerationRequired )
167+ else
118168 {
119- UpdateMarkup ( ) ;
169+ var newMarkup = Htmlizer . GetHtml ( ComponentId , renderer ) ;
170+
171+ // Volatile write is necessary to ensure the updated markup
172+ // is available across CPU cores. Without it, the pointer to the
173+ // markup string can be stored in a CPUs register and not
174+ // get updated when another CPU changes the string.
175+ Volatile . Write ( ref markup , newMarkup ) ;
176+ latestRenderNodes = null ;
120177 OnMarkupUpdated ? . Invoke ( this , EventArgs . Empty ) ;
121178 }
122179
123- // The order here is important, since consumers of the events
124- // expect that markup has indeed changed when OnAfterRender is invoked
125- // (assuming there are markup changes)
126- if ( hasRendered )
127- OnAfterRender ? . Invoke ( this , EventArgs . Empty ) ;
128- }
129-
130- /// <summary>
131- /// Updates the markup of the rendered fragment.
132- /// </summary>
133- private void UpdateMarkup ( )
134- {
135- latestRenderNodes = null ;
136- var newMarkup = Htmlizer . GetHtml ( ComponentId , renderer ) ;
137-
138- // Volatile write is necessary to ensure the updated markup
139- // is available across CPU cores. Without it, the pointer to the
140- // markup string can be stored in a CPUs register and not
141- // get updated when another CPU changes the string.
142- Volatile . Write ( ref markup , newMarkup ) ;
180+ IsDirty = false ;
181+ OnAfterRender ? . Invoke ( this , EventArgs . Empty ) ;
143182 }
144183
145184 /// <summary>
@@ -151,22 +190,5 @@ private void EnsureComponentNotDisposed()
151190 if ( IsDisposed )
152191 throw new ComponentDisposedException ( ComponentId ) ;
153192 }
154-
155- /// <inheritdoc/>
156- public void Dispose ( )
157- {
158- if ( IsDisposed )
159- return ;
160-
161- IsDisposed = true ;
162- markup = string . Empty ;
163- OnAfterRender = null ;
164- OnMarkupUpdated = null ;
165- }
166-
167- public override ValueTask DisposeAsync ( )
168- {
169- Dispose ( ) ;
170- return base . DisposeAsync ( ) ;
171- }
172193}
194+
0 commit comments