Skip to content

Commit 78d4d90

Browse files
egillinkdotnet
andcommitted
refactor: change RenderedComponent to inherit from ComponentState
* refactor: Make RenderedComponent a ComponentState * refactor: change RenderedComponent to inherit from ComponentState --------- Co-authored-by: Steven Giesel <[email protected]>
1 parent 2309b71 commit 78d4d90

File tree

4 files changed

+130
-187
lines changed

4 files changed

+130
-187
lines changed

src/bunit/Rendering/BunitRenderer.cs

+82-120
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
using Microsoft.Extensions.Logging;
1+
using System.Diagnostics;
22
using System.Reflection;
33
using System.Runtime.CompilerServices;
44
using System.Runtime.ExceptionServices;
5+
using Microsoft.Extensions.Logging;
56

67
namespace Bunit.Rendering;
78

@@ -20,7 +21,8 @@ public sealed class BunitRenderer : Renderer
2021
private static extern void CallSetDirectParameters(ComponentState componentState, ParameterView parameters);
2122

2223
private readonly object renderTreeUpdateLock = new();
23-
private readonly Dictionary<int, IRenderedComponent> renderedComponents = new();
24+
25+
private readonly HashSet<int> returnedRenderedComponentIds = new();
2426
private readonly List<BunitRootComponent> rootComponents = new();
2527
private readonly ILogger<BunitRenderer> logger;
2628
private bool disposed;
@@ -182,7 +184,6 @@ public IReadOnlyList<IRenderedComponent<TComponent>> FindComponents<TComponent>(
182184
public Task DisposeComponents()
183185
{
184186
ObjectDisposedException.ThrowIf(disposed, this);
185-
186187
Task? returnTask;
187188

188189
lock (renderTreeUpdateLock)
@@ -206,6 +207,19 @@ public Task DisposeComponents()
206207

207208
return returnTask;
208209
}
210+
/// <inheritdoc/>
211+
protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState)
212+
{
213+
ArgumentNullException.ThrowIfNull(component);
214+
215+
var TComponent = component.GetType();
216+
var renderedComponentType = typeof(RenderedComponent<>).MakeGenericType(TComponent);
217+
var renderedComponent = Activator.CreateInstance(renderedComponentType, this, componentId, component, services, parentComponentState);
218+
219+
Debug.Assert(renderedComponent is not null, "RenderedComponent should not be null");
220+
221+
return (ComponentState)renderedComponent;
222+
}
209223

210224
/// <inheritdoc/>
211225
protected override IComponent ResolveComponentForRenderMode(Type componentType, int? parentComponentId,
@@ -307,118 +321,74 @@ protected override void ProcessPendingRender()
307321
/// <inheritdoc/>
308322
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
309323
{
310-
var renderEvent = new RenderEvent();
311-
PrepareRenderEvent(renderBatch);
312-
ApplyRenderEvent(renderEvent);
313-
314-
return Task.CompletedTask;
324+
for (var i = 0; i < renderBatch.DisposedComponentIDs.Count; i++)
325+
{
326+
var id = renderBatch.DisposedComponentIDs.Array[i];
327+
returnedRenderedComponentIds.Remove(id);
328+
}
315329

316-
void PrepareRenderEvent(in RenderBatch renderBatch)
330+
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
317331
{
318-
for (var i = 0; i < renderBatch.DisposedComponentIDs.Count; i++)
319-
{
320-
var id = renderBatch.DisposedComponentIDs.Array[i];
321-
renderEvent.SetDisposed(id);
322-
}
332+
var diff = renderBatch.UpdatedComponents.Array[i];
333+
var componentState = GetComponentState(diff.ComponentId);
334+
var renderedComponent = (IRenderedComponent)componentState;
323335

324-
for (int i = 0; i < renderBatch.UpdatedComponents.Count; i++)
336+
if (returnedRenderedComponentIds.Contains(diff.ComponentId))
325337
{
326-
ref var update = ref renderBatch.UpdatedComponents.Array[i];
327-
renderEvent.SetUpdated(update.ComponentId, update.Edits.Count > 0);
338+
renderedComponent.UpdateState(hasRendered: true, isMarkupGenerationRequired: diff.Edits.Count > 0);
328339
}
329-
330-
foreach (var (_, rc) in renderedComponents)
340+
else
331341
{
332-
LoadChangesIntoRenderEvent(rc.ComponentId);
342+
renderedComponent.UpdateState(hasRendered: true, false);
333343
}
344+
345+
UpdateParents(diff.Edits.Count > 0, componentState, in renderBatch);
334346
}
335347

336-
void LoadChangesIntoRenderEvent(int componentId)
348+
return Task.CompletedTask;
349+
350+
void UpdateParents(bool hasChanges, ComponentState componentState, in RenderBatch renderBatch)
337351
{
338-
var status = renderEvent.GetOrCreateStatus(componentId);
339-
if (status.FramesLoaded || status.Disposed)
352+
var parent = componentState.ParentComponentState;
353+
if (parent is null)
354+
{
340355
return;
356+
}
341357

342-
var frames = GetCurrentRenderTreeFrames(componentId);
343-
renderEvent.AddFrames(componentId, frames);
344-
345-
for (var i = 0; i < frames.Count; i++)
358+
if (!IsParentComponentAlreadyUpdated(parent.ComponentId, in renderBatch))
346359
{
347-
ref var frame = ref frames.Array[i];
348-
if (frame.FrameType == RenderTreeFrameType.Component)
360+
if (returnedRenderedComponentIds.Contains(parent.ComponentId))
349361
{
350-
// If a child component of the current components has been
351-
// disposed, there is no reason to load the disposed components
352-
// render tree frames. This can also cause a stack overflow if
353-
// the current component was previously a child of the disposed
354-
// component (is that possible?)
355-
var childStatus = renderEvent.GetOrCreateStatus(frame.ComponentId);
356-
if (childStatus.Disposed)
357-
{
358-
logger.LogDisposedChildInRenderTreeFrame(componentId, frame.ComponentId);
359-
}
360-
// The assumption is that a component cannot be in multiple places at
361-
// once. However, in case this is not a correct assumption, this
362-
// ensures that a child components frames are only loaded once.
363-
else if (!renderEvent.GetOrCreateStatus(frame.ComponentId).FramesLoaded)
364-
{
365-
LoadChangesIntoRenderEvent(frame.ComponentId);
366-
}
367-
368-
if (childStatus.Rendered || childStatus.Changed || childStatus.Disposed)
369-
{
370-
status.Rendered = status.Rendered || childStatus.Rendered;
371-
372-
// The current component should also be marked as changed if the child component is
373-
// either changed or disposed, as there is a good chance that the child component
374-
// contained markup which is no longer visible.
375-
status.Changed = status.Changed || childStatus.Changed || childStatus.Disposed;
376-
}
362+
((IRenderedComponent)parent).UpdateState(hasRendered: true, isMarkupGenerationRequired: hasChanges);
377363
}
364+
else
365+
{
366+
((IRenderedComponent)parent).UpdateState(hasRendered: true, false);
367+
}
368+
369+
UpdateParents(hasChanges, parent, in renderBatch);
378370
}
379371
}
380-
}
381-
382-
private void ApplyRenderEvent(RenderEvent renderEvent)
383-
{
384-
RenderCount++;
385372

386-
foreach (var (componentId, status) in renderEvent.Statuses)
373+
static bool IsParentComponentAlreadyUpdated(int componentId, in RenderBatch renderBatch)
387374
{
388-
if (status.UpdatesApplied || !renderedComponents.TryGetValue(componentId, out var rc))
375+
for (var i = 0; i < renderBatch.UpdatedComponents.Count; i++)
389376
{
390-
continue;
391-
}
392-
393-
if (status.Disposed)
394-
{
395-
renderedComponents.Remove(componentId);
396-
rc.OnRender(renderEvent);
397-
renderEvent.SetUpdatedApplied(componentId);
398-
logger.LogComponentDisposed(componentId);
399-
continue;
400-
}
401-
402-
if (status.UpdateNeeded)
403-
{
404-
rc.OnRender(renderEvent);
405-
renderEvent.SetUpdatedApplied(componentId);
406-
407-
// RC can replace the instance of the component it is bound
408-
// to while processing the update event, e.g. during the
409-
// initial render of a component.
410-
if (componentId != rc.ComponentId)
377+
var diff = renderBatch.UpdatedComponents.Array[i];
378+
if (diff.ComponentId == componentId)
411379
{
412-
renderedComponents.Remove(componentId);
413-
renderedComponents.Add(rc.ComponentId, rc);
414-
renderEvent.SetUpdatedApplied(rc.ComponentId);
380+
return diff.Edits.Count > 0;
415381
}
416-
417-
logger.LogComponentRendered(rc.ComponentId);
418382
}
383+
384+
return false;
419385
}
420386
}
421387

388+
/// <inheritdoc/>
389+
internal new ArrayRange<RenderTreeFrame> GetCurrentRenderTreeFrames(int componentId)
390+
=> base.GetCurrentRenderTreeFrames(componentId);
391+
422392
/// <inheritdoc/>
423393
protected override void Dispose(bool disposing)
424394
{
@@ -434,12 +404,7 @@ protected override void Dispose(bool disposing)
434404

435405
if (disposing)
436406
{
437-
foreach (var rc in renderedComponents.Values)
438-
{
439-
rc.Dispose();
440-
}
441-
442-
renderedComponents.Clear();
407+
returnedRenderedComponentIds.Clear();
443408
disposalTasks.Clear();
444409
unhandledExceptionTsc.TrySetCanceled();
445410
}
@@ -448,7 +413,7 @@ protected override void Dispose(bool disposing)
448413
}
449414
}
450415

451-
private RenderedComponent<BunitRootComponent> Render(RenderFragment renderFragment)
416+
private IRenderedComponent<BunitRootComponent> Render(RenderFragment renderFragment)
452417
{
453418
ObjectDisposedException.ThrowIf(disposed, this);
454419

@@ -458,25 +423,26 @@ private RenderedComponent<BunitRootComponent> Render(RenderFragment renderFragme
458423

459424
var root = new BunitRootComponent(renderFragment);
460425
var rootComponentId = AssignRootComponentId(root);
461-
var result = new RenderedComponent<BunitRootComponent>(rootComponentId, root, services);
462-
renderedComponents.Add(rootComponentId, result);
426+
returnedRenderedComponentIds.Add(rootComponentId);
463427
rootComponents.Add(root);
464428
root.Render();
465-
return result;
429+
return rootComponentId;
466430
});
467431

468-
RenderedComponent<BunitRootComponent> result;
432+
int componentId = -1;
469433

470434
if (!renderTask.IsCompleted)
471435
{
472436
logger.LogAsyncInitialRender();
473-
result = renderTask.GetAwaiter().GetResult();
437+
componentId = renderTask.GetAwaiter().GetResult();
474438
}
475439
else
476440
{
477-
result = renderTask.Result;
441+
componentId = renderTask.Result;
478442
}
479443

444+
var result = GetRenderedComponent<BunitRootComponent>(componentId);
445+
480446
logger.LogInitialRenderCompleted(result.ComponentId);
481447

482448
AssertNoUnhandledExceptions();
@@ -488,35 +454,38 @@ private List<IRenderedComponent<TComponent>> FindComponents<TComponent>(IRendere
488454
where TComponent : IComponent
489455
{
490456
ArgumentNullException.ThrowIfNull(parentComponent);
491-
492457
ObjectDisposedException.ThrowIf(disposed, this);
493458

494-
var result = new List<IRenderedComponent<TComponent>>();
495-
var framesCollection = new RenderTreeFrameDictionary();
459+
var result = resultLimit == int.MaxValue
460+
? new List<IRenderedComponent<TComponent>>()
461+
: new List<IRenderedComponent<TComponent>>(resultLimit);
496462

497463
// Blocks the renderer from changing the render tree
498464
// while this method searches through it.
499465
lock (renderTreeUpdateLock)
500466
{
501467
ObjectDisposedException.ThrowIf(disposed, this);
502-
503468
FindComponentsInRenderTree(parentComponent.ComponentId);
469+
foreach (var rc in result)
470+
{
471+
((IRenderedComponent)rc).UpdateState(hasRendered: false, isMarkupGenerationRequired: true);
472+
}
504473
}
505474

506475
return result;
507476

508477
void FindComponentsInRenderTree(int componentId)
509478
{
510-
var frames = GetOrLoadRenderTreeFrame(framesCollection, componentId);
479+
var frames = GetCurrentRenderTreeFrames(componentId);
511480

512481
for (var i = 0; i < frames.Count; i++)
513482
{
514483
ref var frame = ref frames.Array[i];
515484
if (frame.FrameType == RenderTreeFrameType.Component)
516485
{
517-
if (frame.Component is TComponent component)
486+
if (frame.Component is TComponent)
518487
{
519-
result.Add(GetOrCreateRenderedComponent(framesCollection, frame.ComponentId, component));
488+
result.Add(GetRenderedComponent<TComponent>(frame.ComponentId));
520489

521490
if (result.Count == resultLimit)
522491
return;
@@ -531,19 +500,12 @@ void FindComponentsInRenderTree(int componentId)
531500
}
532501
}
533502

534-
private IRenderedComponent<TComponent> GetOrCreateRenderedComponent<TComponent>(RenderTreeFrameDictionary framesCollection, int componentId, TComponent component)
503+
private IRenderedComponent<TComponent> GetRenderedComponent<TComponent>(int componentId)
535504
where TComponent : IComponent
536505
{
537-
if (renderedComponents.TryGetValue(componentId, out var renderedComponent))
538-
{
539-
return (IRenderedComponent<TComponent>)renderedComponent;
540-
}
541-
542-
LoadRenderTreeFrames(componentId, framesCollection);
543-
var result = new RenderedComponent<TComponent>(componentId, component, framesCollection, services);
544-
renderedComponents.Add(result.ComponentId, result);
545-
546-
return result;
506+
var result = GetComponentState(componentId);
507+
returnedRenderedComponentIds.Add(result.ComponentId);
508+
return (IRenderedComponent<TComponent>)result;
547509
}
548510

549511
/// <summary>

src/bunit/Rendering/IRenderedComponent.cs

+1-8
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,7 @@ internal interface IRenderedComponent : IDisposable
1313
/// <summary>
1414
/// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
1515
/// </summary>
16-
/// <param name="renderEvent">A <see cref="RenderEvent"/> that represents a render.</param>
17-
void OnRender(RenderEvent renderEvent);
16+
void UpdateState(bool hasRendered, bool isMarkupGenerationRequired);
1817
}
1918

2019
/// <summary>
@@ -68,10 +67,4 @@ public interface IRenderedComponent<out TComponent> : IDisposable
6867
/// An event that is raised after the markup of the <see cref="IRenderedComponent{TComponent}"/> is updated.
6968
/// </summary>
7069
event EventHandler? OnMarkupUpdated;
71-
72-
/// <summary>
73-
/// Called by the owning <see cref="BunitRenderer"/> when it finishes a render.
74-
/// </summary>
75-
/// <param name="renderEvent">A <see cref="RenderEvent"/> that represents a render.</param>
76-
void OnRender(RenderEvent renderEvent);
7770
}

0 commit comments

Comments
 (0)