Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Callsite rendermodes #48967

Merged
Prev Previous commit
Next Next commit
Actually supply callsite rendermode from RenderTree
  • Loading branch information
SteveSandersonMS committed Jun 23, 2023
commit 12aecc7e276daf27b3b0af7ac7b9201f0f281fb5
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@ private static void InitializeNewComponentFrame(ref DiffContext diffContext, int
var frames = diffContext.NewTree;
ref var frame = ref frames[frameIndex];
var parentComponentId = diffContext.ComponentId;
var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId);
var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(frames, frameIndex, parentComponentId);

// Set initial parameters
var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder);
Expand Down
37 changes: 33 additions & 4 deletions src/Components/Components/src/RenderTree/Renderer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -475,26 +475,55 @@ public Type GetEventArgsType(ulong eventHandlerId)
: EventArgsTypeCache.GetEventArgsType(methodInfo);
}

internal ComponentState InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId)
internal ComponentState InstantiateChildComponentOnFrame(RenderTreeFrame[] frames, int frameIndex, int parentComponentId)
{
ref var frame = ref frames[frameIndex];
if (frame.FrameTypeField != RenderTreeFrameType.Component)
{
throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frame));
throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frameIndex));
}

if (frame.ComponentStateField != null)
{
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame));
throw new ArgumentException($"The frame already has a non-null component instance", nameof(frameIndex));
}

var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, null, parentComponentId);
var callerSpecifiedRenderMode = frame.ComponentFrameFlags.HasFlag(ComponentFrameFlags.HasCallerSpecifiedRenderMode)
? FindCallerSpecifiedRenderMode(frames, frameIndex)
: null;

var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, callerSpecifiedRenderMode, parentComponentId);
var newComponentState = AttachAndInitComponent(newComponent, parentComponentId);
frame.ComponentStateField = newComponentState;
frame.ComponentIdField = newComponentState.ComponentId;

return newComponentState;
}

private static IComponentRenderMode? FindCallerSpecifiedRenderMode(RenderTreeFrame[] frames, int componentFrameIndex)
{
// ComponentRenderMode frames are immediate children of Component frames. So, they have to appear after any parameter
// attributes (since attributes must always immediately follow Component frames), but before anything that would
// represent a different child node, such as text/element or another component. It's OK to do this linear scan
// because we consider it uncommon to specify a rendermode, and none of this happens if you don't.
var endIndex = componentFrameIndex + frames[componentFrameIndex].ComponentSubtreeLengthField;
for (var index = componentFrameIndex + 1; index <= endIndex; index++)
{
ref var frame = ref frames[index];
switch (frame.FrameType)
{
case RenderTreeFrameType.Attribute:
continue;
case RenderTreeFrameType.ComponentRenderMode:
return frame.ComponentRenderMode;
default:
break;
}
}

return null;
}

internal void AddToPendingTasksWithErrorHandling(Task task, ComponentState? owningComponentState)
{
switch (task == null ? TaskStatus.RanToCompletion : task.Status)
Expand Down
45 changes: 44 additions & 1 deletion src/Components/Components/test/RendererTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3639,7 +3639,7 @@ public async Task ExceptionsDispatchedOffSyncContextCanBeHandledAsync()
// Act
renderer.AssignRootComponentId(component);
await component.ExternalExceptionDispatch(exception);

// Assert
Assert.Same(exception, Assert.Single(renderer.HandledExceptions).GetBaseException());
}
Expand Down Expand Up @@ -5232,6 +5232,24 @@ public void ThrowsForUnknownRenderMode_OnComponentType()
Assert.Contains($"Cannot supply a component of type '{typeof(ComponentWithUnknownRenderMode)}' because the current platform does not support the render mode '{typeof(ComponentWithUnknownRenderMode.UnknownRenderMode)}'.", ex.Message);
}

[Fact]
public void ThrowsForUnknownRenderMode_AtCallSite()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenComponent<TestComponent>(0);
builder.AddComponentRenderMode(1, new ComponentWithUnknownRenderMode.UnknownRenderMode());
builder.CloseComponent();
});

// Act
var componentId = renderer.AssignRootComponentId(component);
var ex = Assert.Throws<NotSupportedException>(component.TriggerRender);
Assert.Contains($"Cannot supply a component of type '{typeof(TestComponent)}' because the current platform does not support the render mode '{typeof(ComponentWithUnknownRenderMode.UnknownRenderMode)}'.", ex.Message);
}

[Fact]
public void RenderModeResolverCanSupplyComponent_WithComponentTypeRenderMode()
{
Expand All @@ -5256,6 +5274,31 @@ public void RenderModeResolverCanSupplyComponent_WithComponentTypeRenderMode()
Assert.Equal("Some message", resolvedComponent.Message);
}

[Fact]
public void RenderModeResolverCanSupplyComponent_CallSiteRenderMode()
{
// Arrange
var renderer = new RendererWithRenderModeResolver();

var component = new TestComponent(builder =>
{
builder.OpenComponent<TestComponent>(0);
builder.AddComponentParameter(1, nameof(MessageComponent.Message), "Some message");
builder.AddComponentRenderMode(2, new SubstituteComponentRenderMode());
builder.CloseComponent();
});

// Act
var componentId = renderer.AssignRootComponentId(component);
component.TriggerRender();

// Assert
var batch = renderer.Batches.Single();
var componentFrames = batch.GetComponentFrames<MessageComponent>();
var resolvedComponent = (MessageComponent)componentFrames.Single().Component;
Assert.Equal("Some message", resolvedComponent.Message);
}

[HasSubstituteComponentRenderMode]
private class ComponentWithRenderMode : IComponent
{
Expand Down