Merge branch 'release/3.1' -> 'master'

This commit is contained in:
Doug Bunting 2019-10-14 09:01:59 -07:00
commit 0dedfa95d4
No known key found for this signature in database
GPG Key ID: EE41520987982C03
111 changed files with 1854 additions and 462 deletions

View File

@ -229,7 +229,7 @@
<CastleCorePackageVersion>4.2.1</CastleCorePackageVersion> <CastleCorePackageVersion>4.2.1</CastleCorePackageVersion>
<FSharpCorePackageVersion>4.2.1</FSharpCorePackageVersion> <FSharpCorePackageVersion>4.2.1</FSharpCorePackageVersion>
<GoogleProtobufPackageVersion>3.8.0</GoogleProtobufPackageVersion> <GoogleProtobufPackageVersion>3.8.0</GoogleProtobufPackageVersion>
<GrpcAspNetCorePackageVersion>2.23.1</GrpcAspNetCorePackageVersion> <GrpcAspNetCorePackageVersion>2.23.2</GrpcAspNetCorePackageVersion>
<IdentityServer4AspNetIdentityPackageVersion>3.0.0</IdentityServer4AspNetIdentityPackageVersion> <IdentityServer4AspNetIdentityPackageVersion>3.0.0</IdentityServer4AspNetIdentityPackageVersion>
<IdentityServer4EntityFrameworkPackageVersion>3.0.0</IdentityServer4EntityFrameworkPackageVersion> <IdentityServer4EntityFrameworkPackageVersion>3.0.0</IdentityServer4EntityFrameworkPackageVersion>
<IdentityServer4PackageVersion>3.0.0</IdentityServer4PackageVersion> <IdentityServer4PackageVersion>3.0.0</IdentityServer4PackageVersion>

View File

@ -21,6 +21,8 @@
<PropertyGroup> <PropertyGroup>
<!-- Working around https://github.com/NuGet/Home/issues/8467 --> <!-- Working around https://github.com/NuGet/Home/issues/8467 -->
<NoWarn>$(NoWarn);NU5131</NoWarn> <NoWarn>$(NoWarn);NU5131</NoWarn>
<!-- Workaround until https://github.com/aspnet/AspNetCore-Internal/issues/3103 is resolved -->
<NoWarn>$(NoWarn);NU5048</NoWarn>
</PropertyGroup> </PropertyGroup>
<!-- Workaround https://github.com/dotnet/roslyn/issues/27975 --> <!-- Workaround https://github.com/dotnet/roslyn/issues/27975 -->

View File

@ -96,6 +96,25 @@ app {
color: red; color: red;
} }
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.main .top-row { .main .top-row {
display: none; display: none;

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
@ -8,9 +9,16 @@
<link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" />
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app>Loading...</app> <app>Loading...</app>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script> <script src="_framework/blazor.webassembly.js"></script>
</body> </body>
</html> </html>

View File

@ -1,7 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <!-- Intentionally targeting netcoreapp3.0 because we do not want to update the Benchmark client to use 3.1 -->
<TargetFramework>netcoreapp3.0</TargetFramework>
<IsPackable>true</IsPackable> <IsPackable>true</IsPackable>
<IsShippingPackage>false</IsShippingPackage> <IsShippingPackage>false</IsShippingPackage>
<HasReferenceAssembly>false</HasReferenceAssembly> <HasReferenceAssembly>false</HasReferenceAssembly>

View File

@ -1,4 +1,4 @@
@page "/" @page "/"
<h1>Hello, world!</h1> <h1>Hello, world!</h1>

View File

@ -13,9 +13,7 @@
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app> <component type="typeof(App)" render-mode="ServerPrerendered" />
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
</app>
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
</body> </body>

View File

@ -1,4 +1,4 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
<div class="sidebar"> <div class="sidebar">
<NavMenu /> <NavMenu />
@ -12,4 +12,10 @@
<div class="content px-4"> <div class="content px-4">
@Body @Body
</div> </div>
</div>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a class="reload">Reload</a>
<a class="dismiss">X</a>
</div> </div>

View File

@ -111,6 +111,23 @@ app {
color: red; color: red;
} }
#blazor-error-ui {
background: lightyellow;
position: fixed;
border: "1px solid";
border-color: black;
width: 100%;
bottom: 0;
left: 0;
z-index: 1000;
}
#blazor-error-ui .dismiss {
position: absolute;
right: 5px;
top: 5px;
}
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.main .top-row { .main .top-row {
display: none; display: none;

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,7 @@ import '@dotnet/jsinterop';
import './GlobalExports'; import './GlobalExports';
import * as signalR from '@aspnet/signalr'; import * as signalR from '@aspnet/signalr';
import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack'; import { MessagePackHubProtocol } from '@aspnet/signalr-protocol-msgpack';
import { showErrorNotification } from './BootErrors';
import { shouldAutoStart } from './BootCommon'; import { shouldAutoStart } from './BootCommon';
import { RenderQueue } from './Platform/Circuits/RenderQueue'; import { RenderQueue } from './Platform/Circuits/RenderQueue';
import { ConsoleLogger } from './Platform/Logging/Loggers'; import { ConsoleLogger } from './Platform/Logging/Loggers';
@ -106,6 +107,7 @@ async function initializeConnection(options: BlazorOptions, logger: Logger, circ
connection.on('JS.Error', error => { connection.on('JS.Error', error => {
renderingFailed = true; renderingFailed = true;
unhandledError(connection, error, logger); unhandledError(connection, error, logger);
showErrorNotification();
}); });
window['Blazor']._internal.forceCloseConnection = () => connection.stop(); window['Blazor']._internal.forceCloseConnection = () => connection.stop();

View File

@ -0,0 +1,30 @@
let hasFailed = false;
export async function showErrorNotification() {
let errorUi = document.querySelector('#blazor-error-ui') as HTMLElement;
if (errorUi) {
errorUi.style.display = 'block';
}
if (!hasFailed) {
hasFailed = true;
const errorUiReloads = document.querySelectorAll<HTMLElement>('#blazor-error-ui .reload');
errorUiReloads.forEach(reload => {
reload.onclick = function (e) {
location.reload();
e.preventDefault();
};
});
let errorUiDismiss = document.querySelectorAll<HTMLElement>('#blazor-error-ui .dismiss');
errorUiDismiss.forEach(dismiss => {
dismiss.onclick = function (e) {
const errorUi = document.querySelector<HTMLElement>('#blazor-error-ui');
if (errorUi) {
errorUi.style.display = 'none';
}
e.preventDefault();
};
});
}
}

View File

@ -1,6 +1,7 @@
import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform'; import { MethodHandle, System_Object, System_String, System_Array, Pointer, Platform } from '../Platform';
import { getFileNameFromUrl } from '../Url'; import { getFileNameFromUrl } from '../Url';
import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger'; import { attachDebuggerHotkey, hasDebuggingEnabled } from './MonoDebugger';
import { showErrorNotification } from '../../BootErrors';
const assemblyHandleCache: { [assemblyName: string]: number } = {}; const assemblyHandleCache: { [assemblyName: string]: number } = {};
const typeHandleCache: { [fullyQualifiedTypeName: string]: number } = {}; const typeHandleCache: { [fullyQualifiedTypeName: string]: number } = {};
@ -232,7 +233,11 @@ function createEmscriptenModuleInstance(loadAssemblyUrls: string[], onReady: ()
const suppressMessages = ['DEBUGGING ENABLED']; const suppressMessages = ['DEBUGGING ENABLED'];
module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(`WASM: ${line}`)); module.print = line => (suppressMessages.indexOf(line) < 0 && console.log(`WASM: ${line}`));
module.printErr = line => console.error(`WASM: ${line}`);
module.printErr = line => {
console.error(`WASM: ${line}`);
showErrorNotification();
};
module.preRun = []; module.preRun = [];
module.postRun = []; module.postRun = [];
module.preloadPlugins = []; module.preloadPlugins = [];

View File

@ -341,9 +341,18 @@ export class BrowserRenderer {
} }
} }
private tryApplyValueProperty(batch: RenderBatch, element: Element, attributeFrame: RenderTreeFrame | null) { private tryApplyValueProperty(batch: RenderBatch, element: Element, attributeFrame: RenderTreeFrame | null): boolean {
// Certain elements have built-in behaviour for their 'value' property // Certain elements have built-in behaviour for their 'value' property
const frameReader = batch.frameReader; const frameReader = batch.frameReader;
if (element.tagName === 'INPUT' && element.getAttribute('type') === 'time' && !element.getAttribute('step')) {
const timeValue = attributeFrame ? frameReader.attributeValue(attributeFrame) : null;
if (timeValue) {
element['value'] = timeValue.substring(0, 5);
return true;
}
}
switch (element.tagName) { switch (element.tagName) {
case 'INPUT': case 'INPUT':
case 'SELECT': case 'SELECT':

View File

@ -1,13 +1,19 @@
export class EventForDotNet<TData extends UIEventArgs> { export class EventForDotNet<TData extends UIEventArgs> {
constructor(public readonly type: EventArgsType, public readonly data: TData) { public constructor(public readonly type: EventArgsType, public readonly data: TData) {
} }
static fromDOMEvent(event: Event): EventForDotNet<UIEventArgs> { public static fromDOMEvent(event: Event): EventForDotNet<UIEventArgs> {
const element = event.target as Element; const element = event.target as Element;
switch (event.type) { switch (event.type) {
case 'input': case 'input':
case 'change': { case 'change': {
if (isTimeBasedInput(element)) {
const normalizedValue = normalizeTimeBasedValue(element);
return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: normalizedValue });
}
const targetIsCheckbox = isCheckbox(element); const targetIsCheckbox = isCheckbox(element);
const newValue = targetIsCheckbox ? !!element['checked'] : element['value']; const newValue = targetIsCheckbox ? !!element['checked'] : element['value'];
return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: newValue }); return new EventForDotNet<UIChangeEventArgs>('change', { type: event.type, value: newValue });
@ -36,7 +42,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'keydown': case 'keydown':
case 'keyup': case 'keyup':
case 'keypress': case 'keypress':
return new EventForDotNet<UIKeyboardEventArgs>('keyboard', parseKeyboardEvent(<KeyboardEvent>event)); return new EventForDotNet<UIKeyboardEventArgs>('keyboard', parseKeyboardEvent(event as KeyboardEvent));
case 'contextmenu': case 'contextmenu':
case 'click': case 'click':
@ -46,10 +52,10 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'mousedown': case 'mousedown':
case 'mouseup': case 'mouseup':
case 'dblclick': case 'dblclick':
return new EventForDotNet<UIMouseEventArgs>('mouse', parseMouseEvent(<MouseEvent>event)); return new EventForDotNet<UIMouseEventArgs>('mouse', parseMouseEvent(event as MouseEvent));
case 'error': case 'error':
return new EventForDotNet<UIErrorEventArgs>('error', parseErrorEvent(<ErrorEvent>event)); return new EventForDotNet<UIErrorEventArgs>('error', parseErrorEvent(event as ErrorEvent));
case 'loadstart': case 'loadstart':
case 'timeout': case 'timeout':
@ -57,7 +63,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'load': case 'load':
case 'loadend': case 'loadend':
case 'progress': case 'progress':
return new EventForDotNet<UIProgressEventArgs>('progress', parseProgressEvent(<ProgressEvent>event)); return new EventForDotNet<UIProgressEventArgs>('progress', parseProgressEvent(event as ProgressEvent));
case 'touchcancel': case 'touchcancel':
case 'touchend': case 'touchend':
@ -65,7 +71,7 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'touchenter': case 'touchenter':
case 'touchleave': case 'touchleave':
case 'touchstart': case 'touchstart':
return new EventForDotNet<UITouchEventArgs>('touch', parseTouchEvent(<TouchEvent>event)); return new EventForDotNet<UITouchEventArgs>('touch', parseTouchEvent(event as TouchEvent));
case 'gotpointercapture': case 'gotpointercapture':
case 'lostpointercapture': case 'lostpointercapture':
@ -77,11 +83,11 @@ export class EventForDotNet<TData extends UIEventArgs> {
case 'pointerout': case 'pointerout':
case 'pointerover': case 'pointerover':
case 'pointerup': case 'pointerup':
return new EventForDotNet<UIPointerEventArgs>('pointer', parsePointerEvent(<PointerEvent>event)); return new EventForDotNet<UIPointerEventArgs>('pointer', parsePointerEvent(event as PointerEvent));
case 'wheel': case 'wheel':
case 'mousewheel': case 'mousewheel':
return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(<WheelEvent>event)); return new EventForDotNet<UIWheelEventArgs>('wheel', parseWheelEvent(event as WheelEvent));
default: default:
return new EventForDotNet<UIEventArgs>('unknown', { type: event.type }); return new EventForDotNet<UIEventArgs>('unknown', { type: event.type });
@ -204,8 +210,38 @@ function parseMouseEvent(event: MouseEvent) {
}; };
} }
function isCheckbox(element: Element | null) { function isCheckbox(element: Element | null): boolean {
return element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox'; return !!element && element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
}
const timeBasedInputs = [
'date',
'datetime-local',
'month',
'time',
'week',
];
function isTimeBasedInput(element: Element): element is HTMLInputElement {
return timeBasedInputs.indexOf(element.getAttribute('type')!) !== -1;
}
function normalizeTimeBasedValue(element: HTMLInputElement): string {
const value = element.value;
const type = element.type;
switch (type) {
case 'date':
case 'datetime-local':
case 'month':
return value;
case 'time':
return value.length === 5 ? value + ':00' : value; // Convert hh:mm to hh:mm:00
case 'week':
// For now we are not going to normalize input type week as it is not trivial
return value;
}
throw new Error(`Invalid element type '${type}'.`);
} }
// The following interfaces must be kept in sync with the UIEventArgs C# classes // The following interfaces must be kept in sync with the UIEventArgs C# classes

View File

@ -172,9 +172,15 @@ namespace Microsoft.AspNetCore.Components.Web
[Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")] [Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")] [Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", "value", "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", null, "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", "value", "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", null, "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", "value", "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", null, "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)]
public static partial class BindAttributes public static partial class BindAttributes

View File

@ -172,9 +172,15 @@ namespace Microsoft.AspNetCore.Components.Web
[Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")] [Microsoft.AspNetCore.Components.BindInputElementAttribute("date", "value", "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")] [Microsoft.AspNetCore.Components.BindInputElementAttribute("date", null, "value", "onchange", true, "yyyy-MM-dd")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", "value", "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("datetime-local", null, "value", "onchange", true, "yyyy-MM-ddTHH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", "value", "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("month", null, "value", "onchange", true, "yyyy-MM")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("number", "value", "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("number", null, "value", "onchange", true, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute("text", null, "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", "value", "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("time", null, "value", "onchange", true, "HH:mm:ss")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute(null, "value", "value", "onchange", false, null)]
[Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)] [Microsoft.AspNetCore.Components.BindInputElementAttribute(null, null, "value", "onchange", false, null)]
public static partial class BindAttributes public static partial class BindAttributes

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Components.Forms
// of it for us. We will only get asked to parse the T for nonempty inputs. // of it for us. We will only get asked to parse the T for nonempty inputs.
var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue); var targetType = Nullable.GetUnderlyingType(typeof(TValue)) ?? typeof(TValue);
if (targetType == typeof(int) || if (targetType == typeof(int) ||
targetType == typeof(long) ||
targetType == typeof(float) || targetType == typeof(float) ||
targetType == typeof(double) || targetType == typeof(double) ||
targetType == typeof(decimal)) targetType == typeof(decimal))

View File

@ -30,6 +30,20 @@ namespace Microsoft.AspNetCore.Components.Web
[BindInputElement("date", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")] [BindInputElement("date", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")]
[BindInputElement("date", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")] [BindInputElement("date", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-dd")]
// type="datetime-local" is invariant culture with a specific format.
// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for details.
[BindInputElement("datetime-local", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-ddTHH:mm:ss")]
[BindInputElement("datetime-local", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM-ddTHH:mm:ss")]
// type="month" is invariant culture with a specific format.
// See https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-date-and-time-format-strings for details.
[BindInputElement("month", null, "value", "onchange", isInvariantCulture: true, format: "yyyy-MM")]
[BindInputElement("month", "value", "value", "onchange", isInvariantCulture: true, format: "yyyy-MM")]
// type="time" is invariant culture with a specific format.
[BindInputElement("time", null, "value", "onchange", isInvariantCulture: true, format: "HH:mm:ss")]
[BindInputElement("time", "value", "value", "onchange", isInvariantCulture: true, format: "HH:mm:ss")]
[BindElement("select", null, "value", "onchange")] [BindElement("select", null, "value", "onchange")]
[BindElement("textarea", null, "value", "onchange")] [BindElement("textarea", null, "value", "onchange")]
public static class BindAttributes public static class BindAttributes

View File

@ -0,0 +1,60 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using TestServer;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class ComponentWithParametersTest : ServerTestBase<BasicTestAppServerSiteFixture<PrerenderedStartup>>
{
public ComponentWithParametersTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<PrerenderedStartup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
[Fact]
public void PassingParametersToComponentsFromThePageWorks()
{
Navigate("/prerendered/componentwithparameters?QueryValue=testQueryValue");
BeginInteractivity();
Browser.Exists(By.CssSelector(".interactive"));
var parameter1 = Browser.FindElement(By.CssSelector(".Param1"));
Assert.Equal(100, parameter1.FindElements(By.CssSelector("li")).Count);
Assert.Equal("99 99", parameter1.FindElement(By.CssSelector("li:last-child")).Text);
// The assigned value is of a more derived type than the declared model type. This check
// verifies we use the actual model type during round tripping.
var parameter2 = Browser.FindElement(By.CssSelector(".Param2"));
Assert.Equal("Value Derived-Value", parameter2.Text);
// This check verifies CaptureUnmatchedValues works
var parameter3 = Browser.FindElements(By.CssSelector(".Param3 li"));
Assert.Collection(
parameter3,
p => Assert.Equal("key1 testQueryValue", p.Text),
p => Assert.Equal("key2 43", p.Text));
}
private void BeginInteractivity()
{
Browser.FindElement(By.Id("load-boot-script")).Click();
}
}
}

View File

@ -174,6 +174,18 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
Browser.Equal(9000.ToString(cultureInfo), () => display.Text); Browser.Equal(9000.ToString(cultureInfo), () => display.Text);
Browser.Equal(9000.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value")); Browser.Equal(9000.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
// long
input = Browser.FindElement(By.Id("inputnumber_long"));
display = Browser.FindElement(By.Id("inputnumber_long_value"));
Browser.Equal(4200.ToString(cultureInfo), () => display.Text);
Browser.Equal(4200.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
input.Clear();
input.SendKeys(90000000000.ToString(CultureInfo.InvariantCulture));
input.SendKeys("\t");
Browser.Equal(90000000000.ToString(cultureInfo), () => display.Text);
Browser.Equal(90000000000.ToString(CultureInfo.InvariantCulture), () => input.GetAttribute("value"));
// decimal // decimal
input = Browser.FindElement(By.Id("inputnumber_decimal")); input = Browser.FindElement(By.Id("inputnumber_decimal"));
display = Browser.FindElement(By.Id("inputnumber_decimal_value")); display = Browser.FindElement(By.Id("inputnumber_decimal_value"));

View File

@ -2,10 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Text.Json;
using BasicTestApp; using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures; using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting; using Microsoft.AspNetCore.E2ETesting;
using Moq;
using OpenQA.Selenium; using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Support.UI;
using Xunit; using Xunit;
@ -1018,5 +1020,263 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal(expected.DateTime, () => DateTimeOffset.Parse(boundValue.Text).DateTime); Browser.Equal(expected.DateTime, () => DateTimeOffset.Parse(boundValue.Text).DateTime);
Assert.Equal(expected.DateTime, DateTimeOffset.Parse(mirrorValue.GetAttribute("value")).DateTime); Assert.Equal(expected.DateTime, DateTimeOffset.Parse(mirrorValue.GetAttribute("value")).DateTime);
} }
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindDateTimeLocalTextboxDateTime()
{
var target = Browser.FindElement(By.Id("datetime-local-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("datetime-local-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("datetime-local-textbox-datetime-mirror"));
var expected = new DateTime(1985, 3, 4);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 01/01/0001 because that's the default
target.Clear();
expected = default;
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#datetime-local-textbox-datetime", "2000-01-02T04:05:06");
expected = new DateTime(2000, 1, 2, 04, 05, 06);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindDateTimeLocalTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("datetime-local-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#datetime-local-textbox-nullable-datetime", "2000-01-02T04:05:06");
var expected = new DateTime(2000, 1, 2, 04, 05, 06);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindMonthTextboxDateTime()
{
var target = Browser.FindElement(By.Id("month-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("month-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("month-textbox-datetime-mirror"));
var expected = new DateTime(1985, 3, 1);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
// When the value gets displayed the first time it gets truncated to the 1st day,
// until there is no change the bound value doesn't get updated.
Assert.Equal(expected.AddDays(3), DateTime.Parse(boundValue.Text));
Assert.Equal(expected.AddDays(3), DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 01/01/0001 because that's the default
target.Clear();
expected = default;
Browser.Equal(expected, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#month-textbox-datetime", "2000-02");
expected = new DateTime(2000, 2, 1);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindMonthTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("month-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("month-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("month-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#month-textbox-nullable-datetime", "2000-02");
var expected = new DateTime(2000, 2, 1);
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeTextboxDateTime()
{
var target = Browser.FindElement(By.Id("time-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("time-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-textbox-datetime-mirror"));
var expected = DateTime.Now.Date.AddHours(8).AddMinutes(5);
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(default, DateTime.Parse(boundValue.Text));
Assert.Equal(default, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-textbox-datetime", "04:05");
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("time-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("time-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-textbox-nullable-datetime", "05:06");
var expected = DateTime.Now.Date.Add(new TimeSpan(05, 06, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeStepTextboxDateTime()
{
var target = Browser.FindElement(By.Id("time-step-textbox-datetime"));
var boundValue = Browser.FindElement(By.Id("time-step-textbox-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-step-textbox-datetime-mirror"));
var expected = DateTime.Now.Date.Add(new TimeSpan(8, 5, 30));
Assert.Equal(expected, DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(expected, DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Clear textbox; value updates to 00:00 because that's the default
target.Clear();
expected = default;
Browser.Equal(DateTime.Now.Date, () => DateTime.Parse(target.GetAttribute("value")));
Assert.Equal(default, DateTime.Parse(boundValue.Text));
Assert.Equal(default, DateTime.Parse(mirrorValue.GetAttribute("value")));
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-step-textbox-datetime", "04:05:06");
expected = DateTime.Now.Date.Add(new TimeSpan(4, 5, 6));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
}
// For date comparisons, we parse (non-formatted) values to compare them. Client-side and server-side
// Blazor have different formatting behaviour by default.
[Fact]
public void CanBindTimeStepTextboxNullableDateTime()
{
var target = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime"));
var boundValue = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime-value"));
var mirrorValue = Browser.FindElement(By.Id("time-step-textbox-nullable-datetime-mirror"));
Assert.Equal(string.Empty, target.GetAttribute("value"));
Assert.Equal(string.Empty, boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
Browser.Equal("", () => boundValue.Text);
Assert.Equal("", mirrorValue.GetAttribute("value"));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
// We have to do it this way because the browser gets in the way when sending keys to the input
// element directly.
ApplyInputValue("#time-step-textbox-nullable-datetime", "05:06");
var expected = DateTime.Now.Date.Add(new TimeSpan(05, 06, 0));
Browser.Equal(expected, () => DateTime.Parse(boundValue.Text));
Assert.Equal(expected, DateTime.Parse(mirrorValue.GetAttribute("value")));
// Modify target; verify value is updated and that textboxes linked to the same data are updated
target.Clear();
target.SendKeys("\t");
Browser.Equal(string.Empty, () => boundValue.Text);
Assert.Equal(string.Empty, mirrorValue.GetAttribute("value"));
}
// Applies an input through javascript to datetime-local/month/time controls.
private void ApplyInputValue(string cssSelector, string value)
{
// It's very difficult to enter an invalid value into an <input type=date>, because
// most combinations of keystrokes get normalized to something valid. Additionally,
// using Selenium's SendKeys interacts unpredictably with this normalization logic,
// most likely based on timings. As a workaround, use JS to apply the values. This
// should only be used when strictly necessary, as it doesn't represent actual user
// interaction as authentically as SendKeys in other cases.
var javascript = (IJavaScriptExecutor)Browser;
javascript.ExecuteScript(
$"var elem = document.querySelector('{cssSelector}');"
+ $"elem.value = '{value}';"
+ "elem.dispatchEvent(new KeyboardEvent('change'));");
}
} }
} }

View File

@ -0,0 +1,67 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
public class ErrorNotificationClientSideTest : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
{
public ErrorNotificationClientSideTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
}
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
Browser.MountTestComponent<ErrorComponent>();
Browser.Exists(By.Id("blazor-error-ui"));
Browser.Exists(By.TagName("button"));
}
[Fact]
public void ShowsErrorNotification_OnError_Dismiss()
{
var errorUi = Browser.FindElement(By.Id("blazor-error-ui"));
Assert.Equal("none", errorUi.GetCssValue("display"));
var causeErrorButton = Browser.FindElement(By.TagName("button"));
causeErrorButton.Click();
Browser.Exists(By.CssSelector("#blazor-error-ui[style='display: block;']"), TimeSpan.FromSeconds(10));
var reload = Browser.FindElement(By.ClassName("reload"));
reload.Click();
Browser.DoesNotExist(By.TagName("button"));
}
[Fact]
public void ShowsErrorNotification_OnError_Reload()
{
var causeErrorButton = Browser.Exists(By.TagName("button"));
var errorUi = Browser.FindElement(By.Id("blazor-error-ui"));
Assert.Equal("none", errorUi.GetCssValue("display"));
causeErrorButton.Click();
Browser.Exists(By.CssSelector("#blazor-error-ui[style='display: block;']"));
var dismiss = Browser.FindElement(By.ClassName("dismiss"));
dismiss.Click();
Browser.Exists(By.CssSelector("#blazor-error-ui[style='display: none;']"));
}
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
[Collection("ErrorNotification")] // When the clientside and serverside tests run together it seems to cause failures, possibly due to connection lose on exception.
public class ErrorNotificationServerSideTest : ErrorNotificationClientSideTest
{
public ErrorNotificationServerSideTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}
}
}

View File

@ -11,6 +11,7 @@ using OpenQA.Selenium;
using OpenQA.Selenium.Support.UI; using OpenQA.Selenium.Support.UI;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
using Xunit.Sdk;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{ {
@ -68,6 +69,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["testDtoAsync"] = "Same", ["testDtoAsync"] = "Same",
["returnPrimitiveAsync"] = "123", ["returnPrimitiveAsync"] = "123",
["returnArrayAsync"] = "first,second", ["returnArrayAsync"] = "first,second",
["syncGenericInstanceMethod"] = @"""Initial value""",
["asyncGenericInstanceMethod"] = @"""Updated value 1""",
}; };
var expectedSyncValues = new Dictionary<string, string> var expectedSyncValues = new Dictionary<string, string>
@ -102,6 +105,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
["testDtoSync"] = "Same", ["testDtoSync"] = "Same",
["returnPrimitive"] = "123", ["returnPrimitive"] = "123",
["returnArray"] = "first,second", ["returnArray"] = "first,second",
["genericInstanceMethod"] = @"""Updated value 2""",
}; };
// Include the sync assertions only when running under WebAssembly // Include the sync assertions only when running under WebAssembly
@ -132,13 +136,17 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Assert // Assert
foreach (var expectedValue in expectedValues) foreach (var expectedValue in expectedValues)
{ {
var actualValue = actualValues[expectedValue.Key];
if (expectedValue.Key.Contains("Exception")) if (expectedValue.Key.Contains("Exception"))
{ {
Assert.StartsWith(expectedValue.Value, actualValues[expectedValue.Key]); Assert.StartsWith(expectedValue.Value, actualValue);
} }
else else
{ {
Assert.Equal(expectedValue.Value, actualValues[expectedValue.Key]); if (expectedValue.Value != actualValue)
{
throw new AssertActualExpectedException(expectedValue.Value, actualValue, $"Scenario '{expectedValue.Key}' failed. Expected '{expectedValue.Value}, Actual {actualValue}");
}
} }
} }
} }

View File

@ -254,6 +254,63 @@
<span id="select-markup-box-value">@selectMarkupValue</span> <span id="select-markup-box-value">@selectMarkupValue</span>
</p> </p>
<h2>Time related inputs (datetime-local, month, time)</h2>
<h3>datetime-local</h3>
<p>
DateTime:
<input id="datetime-local-textbox-datetime" @bind="dateTimeLocalTextboxDateTimeValue" type="datetime-local" />
<span id="datetime-local-textbox-datetime-value">@dateTimeLocalTextboxDateTimeValue</span>
<input id="datetime-local-textbox-datetime-mirror" @bind="dateTimeLocalTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="datetime-local-textbox-nullable-datetime" @bind-value="dateTimeLocalTextboxNullableDateTimeValue" type="datetime-local" />
<span id="datetime-local-textbox-nullable-datetime-value">@dateTimeLocalTextboxNullableDateTimeValue</span>
<input id="datetime-local-textbox-nullable-datetime-mirror" @bind="dateTimeLocalTextboxNullableDateTimeValue" readonly />
</p>
<h3>month</h3>
<p>
DateTime:
<input id="month-textbox-datetime" @bind="monthTextboxDateTimeValue" type="month" />
<span id="month-textbox-datetime-value">@monthTextboxDateTimeValue</span>
<input id="month-textbox-datetime-mirror" @bind="monthTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="month-textbox-nullable-datetime" @bind-value="monthTextboxNullableDateTimeValue" type="month" />
<span id="month-textbox-nullable-datetime-value">@monthTextboxNullableDateTimeValue</span>
<input id="month-textbox-nullable-datetime-mirror" @bind="monthTextboxNullableDateTimeValue" readonly />
</p>
<h3>time</h3>
<p>
DateTime:
<input id="time-textbox-datetime" @bind="timeTextboxDateTimeValue" type="time" />
<span id="time-textbox-datetime-value">@timeTextboxDateTimeValue</span>
<input id="time-textbox-datetime-mirror" @bind="timeTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="time-textbox-nullable-datetime" @bind-value="timeTextboxNullableDateTimeValue" type="time" />
<span id="time-textbox-nullable-datetime-value">@timeTextboxNullableDateTimeValue</span>
<input id="time-textbox-nullable-datetime-mirror" @bind="timeTextboxNullableDateTimeValue" readonly />
</p>
<h3>time with step (supports seconds)</h3>
<p>
DateTime:
<input id="time-step-textbox-datetime" @bind="timeStepTextboxDateTimeValue" type="time" step="1" />
<span id="time-step-textbox-datetime-value">@timeStepTextboxDateTimeValue</span>
<input id="time-step-textbox-datetime-mirror" @bind="timeStepTextboxDateTimeValue" readonly />
</p>
<p>
Nullable DateTime:
<input id="time-step-textbox-nullable-datetime" @bind-value="timeStepTextboxNullableDateTimeValue" type="time" step="1" />
<span id="time-step-textbox-nullable-datetime-value">@timeStepTextboxNullableDateTimeValue</span>
<input id="time-step-textbox-nullable-datetime-mirror" @bind="timeStepTextboxNullableDateTimeValue" readonly />
</p>
@code { @code {
string textboxInitiallyBlankValue = null; string textboxInitiallyBlankValue = null;
string textboxInitiallyPopulatedValue = "Hello"; string textboxInitiallyPopulatedValue = "Hello";
@ -297,6 +354,18 @@
DateTime textboxDateTimeFormatInvalidValue = new DateTime(1985, 3, 4); DateTime textboxDateTimeFormatInvalidValue = new DateTime(1985, 3, 4);
DateTimeOffset? textboxNullableDateTimeOffsetFormatInvalidValue = null; DateTimeOffset? textboxNullableDateTimeOffsetFormatInvalidValue = null;
DateTime dateTimeLocalTextboxDateTimeValue = new DateTime(1985, 3, 4);
DateTime? dateTimeLocalTextboxNullableDateTimeValue = null;
DateTime monthTextboxDateTimeValue = new DateTime(1985, 3, 4);
DateTime? monthTextboxNullableDateTimeValue = null;
DateTime timeTextboxDateTimeValue = DateTime.Now.Date.Add(new TimeSpan(8, 5, 0));
DateTime? timeTextboxNullableDateTimeValue = null;
DateTime timeStepTextboxDateTimeValue = DateTime.Now.Date.Add(new TimeSpan(8, 5, 30));
DateTime? timeStepTextboxNullableDateTimeValue = null;
bool includeFourthOption = false; bool includeFourthOption = false;
enum SelectableValue { First, Second, Third, Fourth } enum SelectableValue { First, Second, Third, Fourth }
SelectableValue selectValue = SelectableValue.Second; SelectableValue selectValue = SelectableValue.Second;

View File

@ -0,0 +1,48 @@
<h3 class="interactive">Component With Parameters</h3>
<ul class="Param1">
@foreach (var value in Param1)
{
<li>@value.StringProperty @value.IntProperty</li>
}
</ul>
@* Making sure polymorphism works *@
<div class="Param2">@DerivedParam2.StringProperty @DerivedParam2.DerivedProperty</div>
@* Making sure CaptureUnmatchedValues works *@
<ul class="Param3">
@foreach (var value in Param3.OrderBy(kvp => kvp.Key))
{
<li>@value.Key @value.Value</li>
}
</ul>
@code
{
[Parameter] public List<TestModel> Param1 { get; set; }
[Parameter] public TestModel Param2 { get; set; }
[Parameter(CaptureUnmatchedValues = true)] public IDictionary<string, object> Param3 { get; set; }
private DerivedModel DerivedParam2 => (DerivedModel)Param2;
public static List<TestModel> TestModelValues => Enumerable.Range(0, 100).Select(c => new TestModel { StringProperty = c.ToString(), IntProperty = c }).ToList();
public static DerivedModel DerivedModelValue = new DerivedModel { StringProperty = "Value", DerivedProperty = "Derived-Value" };
public class TestModel
{
public string StringProperty { get; set; }
public int IntProperty { get; set; }
}
public class DerivedModel : TestModel
{
public string DerivedProperty { get; set; }
}
}

View File

@ -0,0 +1,16 @@
<div>
<h2>Error throwing button</h2>
<p>
<button @onclick="@(IncrementCount)">Click me</button>
</p>
</div>
@code {
int currentCount = 0;
void IncrementCount()
{
currentCount++;
throw new NotImplementedException("Doing crazy things!");
}
}

View File

@ -62,6 +62,10 @@
int: <InputNumber id="inputnumber_int" @bind-Value="inputNumberInt" /> int: <InputNumber id="inputnumber_int" @bind-Value="inputNumberInt" />
<span id="inputnumber_int_value">@inputNumberInt</span> <span id="inputnumber_int_value">@inputNumberInt</span>
</div> </div>
<div>
long: <InputNumber id="inputnumber_long" @bind-Value="inputNumberLong" />
<span id="inputnumber_long_value">@inputNumberLong</span>
</div>
<div> <div>
decimal: <InputNumber id="inputnumber_decimal" @bind-Value="inputNumberDecimal" /> decimal: <InputNumber id="inputnumber_decimal" @bind-Value="inputNumberDecimal" />
<span id="inputnumber_decimal_value">@inputNumberDecimal</span> <span id="inputnumber_decimal_value">@inputNumberDecimal</span>
@ -99,8 +103,18 @@
DateTimeOffset inputTypeDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4)); DateTimeOffset inputTypeDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
int inputNumberInt = 42; int inputNumberInt = 42;
long inputNumberLong = 4200;
decimal inputNumberDecimal = 4.2m; decimal inputNumberDecimal = 4.2m;
DateTime inputDateDateTime = new DateTime(1985, 3, 4); DateTime inputDateDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4)); DateTimeOffset inputDateDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeDateTimeLocalDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeDateTimeLocalDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeMonthDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeMonthDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
DateTime inputTypeTimeDateTime = new DateTime(1985, 3, 4);
DateTimeOffset inputTypeTimeDateTimeOffset = new DateTimeOffset(new DateTime(1985, 3, 4));
} }

View File

@ -1,6 +1,6 @@
@using Microsoft.AspNetCore.Components.Rendering @using Microsoft.AspNetCore.Components.Rendering
<div id="test-selector"> <div id="test-selector">
Select test: Select test:
<select id="test-selector-select" @bind=SelectedComponentTypeName> <select id="test-selector-select" @bind=SelectedComponentTypeName>
<option value="none">Choose...</option> <option value="none">Choose...</option>
<option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option> <option value="BasicTestApp.AddRemoveChildComponents">Add/remove child components</option>
@ -20,6 +20,7 @@
<option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option> <option value="BasicTestApp.DispatchingComponent">Dispatching to sync context</option>
<option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option> <option value="BasicTestApp.DuplicateAttributesComponent">Duplicate attributes</option>
<option value="BasicTestApp.ElementRefComponent">Element ref component</option> <option value="BasicTestApp.ElementRefComponent">Element ref component</option>
<option value="BasicTestApp.ErrorComponent">Error throwing</option>
<option value="BasicTestApp.EventBubblingComponent">Event bubbling</option> <option value="BasicTestApp.EventBubblingComponent">Event bubbling</option>
<option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option> <option value="BasicTestApp.EventCallbackTest.EventCallbackCases">EventCallback</option>
<option value="BasicTestApp.EventCasesComponent">Event cases</option> <option value="BasicTestApp.EventCasesComponent">Event cases</option>
@ -82,6 +83,12 @@
@((RenderFragment)RenderSelectedComponent) @((RenderFragment)RenderSelectedComponent)
</app> </app>
<div id="blazor-error-ui">
An unhandled error has occurred.
<a href class='reload'>Reload</a>
<a class='dismiss' style="cursor: pointer;">🗙</a>
</div>
@code { @code {
string SelectedComponentTypeName { get; set; } = "none"; string SelectedComponentTypeName { get; set; } = "none";

View File

@ -70,13 +70,15 @@
var shouldSupportSyncInterop = RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY")); var shouldSupportSyncInterop = RuntimeInformation.IsOSPlatform(OSPlatform.Create("WEBASSEMBLY"));
var testDTOTOPassByRef = new TestDTO(nonSerializedValue: 123); var testDTOTOPassByRef = new TestDTO(nonSerializedValue: 123);
var instanceMethodsTarget = new JavaScriptInterop(); var instanceMethodsTarget = new JavaScriptInterop();
var genericType = new JavaScriptInterop.GenericType<string> { Value = "Initial value" };
Console.WriteLine("Starting interop invocations."); Console.WriteLine("Starting interop invocations.");
await JSRuntime.InvokeVoidAsync( await JSRuntime.InvokeVoidAsync(
"jsInteropTests.invokeDotNetInteropMethodsAsync", "jsInteropTests.invokeDotNetInteropMethodsAsync",
shouldSupportSyncInterop, shouldSupportSyncInterop,
DotNetObjectReference.Create(testDTOTOPassByRef), DotNetObjectReference.Create(testDTOTOPassByRef),
DotNetObjectReference.Create(instanceMethodsTarget)); DotNetObjectReference.Create(instanceMethodsTarget),
DotNetObjectReference.Create(genericType));
if (shouldSupportSyncInterop) if (shouldSupportSyncInterop)
{ {

View File

@ -462,5 +462,25 @@ namespace BasicTestApp.InteropTest
public DotNetObjectReference<TestDTO> OutgoingByRef { get; set; } public DotNetObjectReference<TestDTO> OutgoingByRef { get; set; }
} }
public class GenericType<TValue>
{
public TValue Value { get; set; }
[JSInvokable]
public TValue Update(TValue newValue)
{
var oldValue = Value;
Value = newValue;
return oldValue;
}
[JSInvokable]
public async Task<TValue> UpdateAsync(TValue newValue)
{
await Task.Yield();
return Update(newValue);
}
}
} }
} }

View File

@ -1,5 +1,6 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Basic test app</title> <title>Basic test app</title>
@ -9,6 +10,7 @@
<!-- Used by ExternalContentPackage --> <!-- Used by ExternalContentPackage -->
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" /> <link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head> </head>
<body> <body>
<root>Loading...</root> <root>Loading...</root>
@ -31,4 +33,5 @@
<!-- Used by ExternalContentPackage --> <!-- Used by ExternalContentPackage -->
<script src="_content/TestContentPackage/prompt.js"></script> <script src="_content/TestContentPackage/prompt.js"></script>
</body> </body>
</html> </html>

View File

@ -2,7 +2,7 @@
var results = {}; var results = {};
var assemblyName = 'BasicTestApp'; var assemblyName = 'BasicTestApp';
function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectByRef, instanceMethodsTarget) { async function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectByRef, instanceMethodsTarget, genericDotNetObjectByRef) {
if (shouldSupportSyncInterop) { if (shouldSupportSyncInterop) {
console.log('Invoking void sync methods.'); console.log('Invoking void sync methods.');
DotNet.invokeMethod(assemblyName, 'VoidParameterless'); DotNet.invokeMethod(assemblyName, 'VoidParameterless');
@ -15,6 +15,7 @@ function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectB
DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7, dotNetObjectByRef)); DotNet.invokeMethod(assemblyName, 'VoidWithSevenParameters', ...createArgumentList(7, dotNetObjectByRef));
DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8, dotNetObjectByRef)); DotNet.invokeMethod(assemblyName, 'VoidWithEightParameters', ...createArgumentList(8, dotNetObjectByRef));
console.log('Invoking returning sync methods.'); console.log('Invoking returning sync methods.');
results['result1'] = DotNet.invokeMethod(assemblyName, 'ReturnArray'); results['result1'] = DotNet.invokeMethod(assemblyName, 'ReturnArray');
results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1, dotNetObjectByRef)); results['result2'] = DotNet.invokeMethod(assemblyName, 'EchoOneParameter', ...createArgumentList(1, dotNetObjectByRef));
@ -40,75 +41,73 @@ function invokeDotNetInteropMethodsAsync(shouldSupportSyncInterop, dotNetObjectB
} }
console.log('Invoking void async methods.'); console.log('Invoking void async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync')
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef)))
.then(() => DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(() => {
console.log('Invoking returning async methods.');
return DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync')
.then(r => results['result1Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef)))
.then(r => results['result2Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef)))
.then(r => results['result3Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef)))
.then(r => results['result4Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef)))
.then(r => results['result5Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef)))
.then(r => results['result6Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef)))
.then(r => results['result7Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef)))
.then(r => results['result8Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef)))
.then(r => results['result9Async'] = r)
.then(() => DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync'))
.then(r => DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', r['Some async instance']))
.then(r => {
results['resultReturnDotNetObjectByRefAsync'] = r;
})
.then(() => instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
}))
.then(r => {
results['instanceMethodThisTypeNameAsync'] = r.thisTypeName;
results['instanceMethodStringValueUpperAsync'] = r.stringValueUpper;
results['instanceMethodIncomingByRefAsync'] = r.incomingByRef;
return DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', r.outgoingByRef);
}).then(r => {
results['instanceMethodOutgoingByRefAsync'] = r;
})
})
.then(() => {
console.log('Invoking methods that throw exceptions');
try {
shouldSupportSyncInterop && DotNet.invokeMethod(assemblyName, 'ThrowException');
} catch (e) {
results['ThrowException'] = e.message;
}
return DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowSyncException') await DotNet.invokeMethodAsync(assemblyName, 'VoidParameterlessAsync');
.catch(e => { await DotNet.invokeMethodAsync(assemblyName, 'VoidWithOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef));
results['AsyncThrowSyncException'] = e.message; await DotNet.invokeMethodAsync(assemblyName, 'VoidWithTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef));
await DotNet.invokeMethodAsync(assemblyName, 'VoidWithEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef));
return DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowAsyncException'); console.log('Invoking returning async methods.');
}).catch(e => { results['result1Async'] = await DotNet.invokeMethodAsync(assemblyName, 'ReturnArrayAsync');
results['AsyncThrowAsyncException'] = e.message; results['result2Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoOneParameterAsync', ...createArgumentList(1, dotNetObjectByRef));
results['result3Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoTwoParametersAsync', ...createArgumentList(2, dotNetObjectByRef));
results['result4Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoThreeParametersAsync', ...createArgumentList(3, dotNetObjectByRef));
results['result5Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoFourParametersAsync', ...createArgumentList(4, dotNetObjectByRef));
results['result6Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoFiveParametersAsync', ...createArgumentList(5, dotNetObjectByRef));
results['result7Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoSixParametersAsync', ...createArgumentList(6, dotNetObjectByRef));
results['result8Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoSevenParametersAsync', ...createArgumentList(7, dotNetObjectByRef));
results['result9Async'] = await DotNet.invokeMethodAsync(assemblyName, 'EchoEightParametersAsync', ...createArgumentList(8, dotNetObjectByRef));
console.log('Done invoking interop methods'); const returnDotNetObjectByRefAsync = await DotNet.invokeMethodAsync(assemblyName, 'ReturnDotNetObjectByRefAsync');
}); results['resultReturnDotNetObjectByRefAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', returnDotNetObjectByRefAsync['Some async instance']);
});
const instanceMethodAsync = await instanceMethodsTarget.invokeMethodAsync('InstanceMethodAsync', {
stringValue: 'My string',
dtoByRef: dotNetObjectByRef
});
results['instanceMethodThisTypeNameAsync'] = instanceMethodAsync.thisTypeName;
results['instanceMethodStringValueUpperAsync'] = instanceMethodAsync.stringValueUpper;
results['instanceMethodIncomingByRefAsync'] = instanceMethodAsync.incomingByRef;
results['instanceMethodOutgoingByRefAsync'] = await DotNet.invokeMethodAsync(assemblyName, 'ExtractNonSerializedValue', instanceMethodAsync.outgoingByRef);
console.log('Invoking generic type instance methods.');
results['syncGenericInstanceMethod'] = await genericDotNetObjectByRef.invokeMethodAsync('Update', 'Updated value 1');
results['asyncGenericInstanceMethod'] = await genericDotNetObjectByRef.invokeMethodAsync('UpdateAsync', 'Updated value 2');
if (shouldSupportSyncInterop) {
results['genericInstanceMethod'] = genericDotNetObjectByRef.invokeMethod('Update', 'Updated Value 3');
}
console.log('Invoking methods that throw exceptions');
try {
shouldSupportSyncInterop && DotNet.invokeMethod(assemblyName, 'ThrowException');
} catch (e) {
results['ThrowException'] = e.message;
}
try {
await DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowSyncException');
} catch (e) {
results['AsyncThrowSyncException'] = e.message;
}
try {
await DotNet.invokeMethodAsync(assemblyName, 'AsyncThrowAsyncException');
} catch (e) {
results['AsyncThrowAsyncException'] = e.message;
}
console.log('Done invoking interop methods');
} }
function createArgumentList(argumentNumber, dotNetObjectByRef){ function createArgumentList(argumentNumber, dotNetObjectByRef) {
const array = new Array(argumentNumber); const array = new Array(argumentNumber);
if (argumentNumber === 0) { if (argumentNumber === 0) {
return []; return [];
@ -149,7 +148,7 @@ function createArgumentList(argumentNumber, dotNetObjectByRef){
source: `Some random text with at least ${i} characters`, source: `Some random text with at least ${i} characters`,
start: argumentNumber + 1, start: argumentNumber + 1,
length: argumentNumber + 1 length: argumentNumber + 1
} };
break; break;
default: default:
console.log(i); console.log(i);
@ -169,7 +168,7 @@ window.jsInteropTests = {
returnPrimitive: returnPrimitive, returnPrimitive: returnPrimitive,
returnPrimitiveAsync: returnPrimitiveAsync, returnPrimitiveAsync: returnPrimitiveAsync,
receiveDotNetObjectByRef: receiveDotNetObjectByRef, receiveDotNetObjectByRef: receiveDotNetObjectByRef,
receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync, receiveDotNetObjectByRefAsync: receiveDotNetObjectByRefAsync
}; };
function returnPrimitive() { function returnPrimitive() {
@ -211,9 +210,9 @@ function asyncFunctionThrowsAsyncException() {
} }
function asyncFunctionTakesLongerThanDefaultTimeoutToResolve() { function asyncFunctionTakesLongerThanDefaultTimeoutToResolve() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(() => resolve(undefined), 5000); setTimeout(() => resolve(undefined), 5000);
}); });
} }
function collectInteropResults() { function collectInteropResults() {

View File

@ -6,6 +6,14 @@
outline: 1px solid red; outline: 1px solid red;
} }
#blazor-error-ui {
display: none;
}
#blazor-error-ui dismiss {
cursor: pointer;
}
.validation-message { .validation-message {
color: red; color: red;
} }

View File

@ -12,7 +12,7 @@
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<App>(RenderMode.Server))</app> <component type="typeof(App)" render-mode="Server" />
<script src="_framework/blazor.server.js" autostart="false"></script> <script src="_framework/blazor.server.js" autostart="false"></script>
<script> <script>
Blazor.start({ Blazor.start({

View File

@ -0,0 +1,37 @@
@page
<component type="typeof(ComponentWithParameters)"
render-mode="ServerPrerendered"
param-Param1="ComponentWithParameters.TestModelValues"
param-Param2="ComponentWithParameters.DerivedModelValue"
param-key1="QueryValue"
param-key2="43" />
@*
So that E2E tests can make assertions about both the prerendered and
interactive states, we only load the .js file when told to.
*@
<hr />
<button id="load-boot-script" onclick="start()">Load boot script</button>
<script src="_framework/blazor.server.js" autostart="false"></script>
<script>
// Used by InteropOnInitializationComponent
function setElementValue(element, newValue) {
element.value = newValue;
return element.value;
}
function start() {
Blazor.start({
logLevel: 1 // LogLevel.Debug
});
}
</script>
@functions
{
[BindProperty(SupportsGet = true)]
public string QueryValue { get; set; }
}

View File

@ -12,24 +12,24 @@
<div id="test-container"> <div id="test-container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered)) @(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) @(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server))
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Static, new { Name = "John" })) <component type="typeof(GreeterComponent)" render-mode="Static" param-name='"John"' />
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) <component type="typeof(GreeterComponent)" render-mode="Server"/>
<div id="container"> <div id="container">
<p>Some content before</p> <p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) <component type="typeof(GreeterComponent)" render-mode="Server"/>
<p>Some content between</p> <p>Some content between</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered)) <component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p> <p>Some content after</p>
<div id="nested-an-extra-level"> <div id="nested-an-extra-level">
<p>Some content before</p> <p>Some content before</p>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server)) <component type="typeof(GreeterComponent)" render-mode="Server"/>
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered)) <component type="typeof(GreeterComponent)" render-mode="ServerPrerendered"/>
<p>Some content after</p> <p>Some content after</p>
</div> </div>
</div> </div>
<div id="container"> <div id="container">
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.Server, new { Name = "Albert" })) <component type="typeof(GreeterComponent)" render-mode="Server" param-name='"Albert"' />
@(await Html.RenderComponentAsync<GreeterComponent>(RenderMode.ServerPrerendered, new { Name = "Abraham" })) <component type="typeof(GreeterComponent)" render-mode="ServerPrerendered" param-name='"Abraham"' />
</div> </div>
</div> </div>

View File

@ -1,5 +1,6 @@
@page @page
@using BasicTestApp.RouterTest @using BasicTestApp.RouterTest
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -7,7 +8,7 @@
<base href="~/" /> <base href="~/" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<TestRouter>(RenderMode.ServerPrerendered))</app> <app><component type="typeof(TestRouter)" render-mode="ServerPrerendered" /></app>
@* @*
So that E2E tests can make assertions about both the prerendered and So that E2E tests can make assertions about both the prerendered and

View File

@ -1,4 +1,5 @@
@page "" @page ""
@addTagHelper "*, Microsoft.AspNetCore.Mvc.TagHelpers"
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
@ -11,7 +12,7 @@
<link href="_content/TestContentPackage/styles.css" rel="stylesheet" /> <link href="_content/TestContentPackage/styles.css" rel="stylesheet" />
</head> </head>
<body> <body>
<root>@(await Html.RenderComponentAsync<BasicTestApp.Index>(RenderMode.Server))</root> <root><component type="typeof(BasicTestApp.Index)" render-mode="Server" /></root>
<!-- Used for testing interop scenarios between JS and .NET --> <!-- Used for testing interop scenarios between JS and .NET -->
<script src="js/jsinteroptests.js"></script> <script src="js/jsinteroptests.js"></script>

View File

@ -0,0 +1,3 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@using BasicTestApp

View File

@ -3221,6 +3221,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
public partial class ValidationVisitor public partial class ValidationVisitor
{ {
public ValidationVisitor(Microsoft.AspNetCore.Mvc.ActionContext actionContext, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IModelValidatorProvider validatorProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache validatorCache, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationStateDictionary validationState) { } public ValidationVisitor(Microsoft.AspNetCore.Mvc.ActionContext actionContext, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.IModelValidatorProvider validatorProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache validatorCache, Microsoft.AspNetCore.Mvc.ModelBinding.IModelMetadataProvider metadataProvider, Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidationStateDictionary validationState) { }
[System.ObsoleteAttribute("This property is deprecated and is no longer used by the runtime.")]
public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
protected Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache Cache { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } } protected Microsoft.AspNetCore.Mvc.ModelBinding.Validation.ValidatorCache Cache { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
protected object Container { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } protected object Container { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

View File

@ -1894,7 +1894,10 @@ namespace Microsoft.AspNetCore.Mvc
detail: detail, detail: detail,
instance: instance); instance: instance);
return new ObjectResult(problemDetails); return new ObjectResult(problemDetails)
{
StatusCode = problemDetails.Status
};
} }
/// <summary> /// <summary>
@ -1970,7 +1973,10 @@ namespace Microsoft.AspNetCore.Mvc
return new BadRequestObjectResult(validationProblem); return new BadRequestObjectResult(validationProblem);
} }
return new ObjectResult(validationProblem); return new ObjectResult(validationProblem)
{
StatusCode = validationProblem.Status
};
} }
/// <summary> /// <summary>

View File

@ -109,6 +109,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Validation
/// </summary> /// </summary>
/// <value>The default value is <see langword="true"/>.</value> /// <value>The default value is <see langword="true"/>.</value>
/// <remarks>This property is currently ignored.</remarks> /// <remarks>This property is currently ignored.</remarks>
[Obsolete("This property is deprecated and is no longer used by the runtime.")]
public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent { get; set; } = true; public bool AllowShortCircuitingValidationWhenNoValidatorsArePresent { get; set; } = true;
/// <summary> /// <summary>

View File

@ -2316,6 +2316,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert // Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(actionResult); var badRequestResult = Assert.IsType<BadRequestObjectResult>(actionResult);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequestResult.Value); var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequestResult.Value);
Assert.Equal(400, badRequestResult.StatusCode);
Assert.Equal(400, problemDetails.Status); Assert.Equal(400, problemDetails.Status);
Assert.Equal("One or more validation errors occurred.", problemDetails.Title); Assert.Equal("One or more validation errors occurred.", problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type); Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.5.1", problemDetails.Type);
@ -2348,6 +2349,48 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
Assert.Equal(detail, problemDetails.Detail); Assert.Equal(detail, problemDetails.Detail);
} }
[Fact]
public void ValidationProblemDetails_UsesSpecifiedStatusCode()
{
// Arrange
var options = GetApiBehaviorOptions();
var controller = new TestableController
{
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
};
// Act
var actionResult = controller.ValidationProblem(statusCode: 405);
// Assert
var objectResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ValidationProblemDetails>(objectResult.Value);
Assert.Equal(405, objectResult.StatusCode);
Assert.Equal(405, problemDetails.Status);
}
[Fact]
public void ValidationProblemDetails_StatusCode400_ReturnsBadRequestObjectResultFor2xCompatibility()
{
// Arrange
var options = GetApiBehaviorOptions();
var controller = new TestableController
{
ProblemDetailsFactory = new DefaultProblemDetailsFactory(Options.Create(options)),
};
// Act
var actionResult = controller.ValidationProblem(statusCode: 400);
// Assert
var badRequestResult = Assert.IsType<BadRequestObjectResult>(actionResult);
var problemDetails = Assert.IsType<ValidationProblemDetails>(badRequestResult.Value);
Assert.Equal(400, badRequestResult.StatusCode);
Assert.Equal(400, problemDetails.Status);
}
[Fact] [Fact]
public void ProblemDetails_Works() public void ProblemDetails_Works()
{ {
@ -2371,6 +2414,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert // Assert
var badRequestResult = Assert.IsType<ObjectResult>(actionResult); var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value); var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
Assert.Equal(500, actionResult.StatusCode);
Assert.Equal(500, problemDetails.Status); Assert.Equal(500, problemDetails.Status);
Assert.Equal("An error occured while processing your request.", problemDetails.Title); Assert.Equal("An error occured while processing your request.", problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
@ -2396,6 +2440,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert // Assert
var badRequestResult = Assert.IsType<ObjectResult>(actionResult); var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value); var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
Assert.Equal(500, actionResult.StatusCode);
Assert.Equal(500, problemDetails.Status); Assert.Equal(500, problemDetails.Status);
Assert.Equal(title, problemDetails.Title); Assert.Equal(title, problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type); Assert.Equal("https://tools.ietf.org/html/rfc7231#section-6.6.1", problemDetails.Type);
@ -2419,6 +2464,7 @@ namespace Microsoft.AspNetCore.Mvc.Core.Test
// Assert // Assert
var badRequestResult = Assert.IsType<ObjectResult>(actionResult); var badRequestResult = Assert.IsType<ObjectResult>(actionResult);
var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value); var problemDetails = Assert.IsType<ProblemDetails>(badRequestResult.Value);
Assert.Equal(422, actionResult.StatusCode);
Assert.Equal(422, problemDetails.Status); Assert.Equal(422, problemDetails.Status);
Assert.Equal("Unprocessable entity.", problemDetails.Title); Assert.Equal("Unprocessable entity.", problemDetails.Title);
Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type); Assert.Equal("https://tools.ietf.org/html/rfc4918#section-11.2", problemDetails.Type);

View File

@ -12,7 +12,9 @@ using System.Threading.Tasks;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Logging.Testing;
using Microsoft.AspNetCore.WebUtilities;
using Newtonsoft.Json; using Newtonsoft.Json;
using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.Formatters namespace Microsoft.AspNetCore.Mvc.Formatters
@ -462,6 +464,30 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
Assert.Single(formatterContext.ModelState["Person.Name"].Errors); Assert.Single(formatterContext.ModelState["Person.Name"].Errors);
} }
[Fact]
public async Task ReadAsync_DoesNotDisposeBufferedReadStream()
{
// Arrange
var formatter = GetInputFormatter();
var content = "{\"name\": \"Test\"}";
var contentBytes = Encoding.UTF8.GetBytes(content);
var httpContext = GetHttpContext(contentBytes);
var testBufferedReadStream = new Mock<FileBufferingReadStream>(httpContext.Request.Body, 1024) { CallBase = true };
httpContext.Request.Body = testBufferedReadStream.Object;
var formatterContext = CreateInputFormatterContext(typeof(ComplexModel), httpContext);
// Act
var result = await formatter.ReadAsync(formatterContext);
// Assert
var userModel = Assert.IsType<ComplexModel>(result.Model);
Assert.Equal("Test", userModel.Name);
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
}
internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; } internal abstract string JsonFormatter_EscapedKeys_Bracket_Expected { get; }
internal abstract string JsonFormatter_EscapedKeys_Expected { get; } internal abstract string JsonFormatter_EscapedKeys_Expected { get; }
@ -517,7 +543,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
protected sealed class ComplexPoco protected sealed class ComplexPoco
{ {
public int Id { get; set; } public int Id { get; set; }
public Person Person{ get; set; } public Person Person { get; set; }
} }
protected sealed class Person protected sealed class Person

View File

@ -118,6 +118,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
Stream readStream = new NonDisposableStream(request.Body); Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering) if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering)
{ {
@ -135,6 +136,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
await readStream.DrainAsync(CancellationToken.None); await readStream.DrainAsync(CancellationToken.None);
readStream.Seek(0L, SeekOrigin.Begin); readStream.Seek(0L, SeekOrigin.Begin);
disposeReadStream = true;
} }
try try
@ -162,9 +165,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
finally finally
{ {
if (readStream is FileBufferingReadStream fileBufferingReadStream) if (disposeReadStream)
{ {
await fileBufferingReadStream.DisposeAsync(); await readStream.DisposeAsync();
} }
} }
} }

View File

@ -99,6 +99,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var request = context.HttpContext.Request; var request = context.HttpContext.Request;
Stream readStream = new NonDisposableStream(request.Body); Stream readStream = new NonDisposableStream(request.Body);
var disposeReadStream = false;
if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering) if (!request.Body.CanSeek && !_options.SuppressInputFormatterBuffering)
{ {
// XmlSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously // XmlSerializer does synchronous reads. In order to avoid blocking on the stream, we asynchronously
@ -115,6 +117,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
await readStream.DrainAsync(CancellationToken.None); await readStream.DrainAsync(CancellationToken.None);
readStream.Seek(0L, SeekOrigin.Begin); readStream.Seek(0L, SeekOrigin.Begin);
disposeReadStream = true;
} }
try try
@ -155,9 +158,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
} }
finally finally
{ {
if (readStream is FileBufferingReadStream fileBufferingReadStream) if (disposeReadStream)
{ {
await fileBufferingReadStream.DisposeAsync(); await readStream.DisposeAsync();
} }
} }
} }

View File

@ -12,6 +12,7 @@ using System.Xml;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.WebUtilities;
using Moq; using Moq;
using Xunit; using Xunit;
@ -166,6 +167,39 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
Assert.Equal(expectedString, model.sampleString); Assert.Equal(expectedString, model.sampleString);
} }
[Fact]
public async Task ReadAsync_DoesNotDisposeBufferedStreamIfItDidNotCreateIt()
{
// Arrange
var expectedInt = 10;
var expectedString = "TestString";
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
"<sampleString>" + expectedString + "</sampleString></TestLevelOne>";
var formatter = new XmlDataContractSerializerInputFormatter(new MvcOptions());
var contentBytes = Encoding.UTF8.GetBytes(input);
var httpContext = new DefaultHttpContext();
var testBufferedReadStream = new Mock<FileBufferingReadStream>(new MemoryStream(contentBytes), 1024) { CallBase = true };
httpContext.Request.Body = testBufferedReadStream.Object;
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
// Act
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
}
[Fact] [Fact]
public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody() public async Task SuppressInputFormatterBufferingSetToTrue_DoesNotBufferRequestBody()
{ {

View File

@ -13,6 +13,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Testing; using Microsoft.AspNetCore.Testing;
using Microsoft.AspNetCore.WebUtilities;
using Moq; using Moq;
using Xunit; using Xunit;
@ -622,6 +623,39 @@ namespace Microsoft.AspNetCore.Mvc.Formatters.Xml
Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), model.SampleDate); Assert.Equal(XmlConvert.ToDateTime(expectedDateTime, XmlDateTimeSerializationMode.Utc), model.SampleDate);
} }
[Fact]
public async Task ReadAsync_DoesNotDisposeBufferedStreamIfItDidNotCreateIt()
{
// Arrange
var expectedInt = 10;
var expectedString = "TestString";
var input = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
"<TestLevelOne><SampleInt>" + expectedInt + "</SampleInt>" +
"<sampleString>" + expectedString + "</sampleString></TestLevelOne>";
var formatter = new XmlSerializerInputFormatter(new MvcOptions());
var contentBytes = Encoding.UTF8.GetBytes(input);
var httpContext = new DefaultHttpContext();
var testBufferedReadStream = new Mock<FileBufferingReadStream>(new MemoryStream(contentBytes), 1024) { CallBase = true };
httpContext.Request.Body = testBufferedReadStream.Object;
var context = GetInputFormatterContext(httpContext, typeof(TestLevelOne));
// Act
var result = await formatter.ReadAsync(context);
// Assert
Assert.NotNull(result);
Assert.False(result.HasError);
var model = Assert.IsType<TestLevelOne>(result.Model);
Assert.Equal(expectedInt, model.SampleInt);
Assert.Equal(expectedString, model.sampleString);
testBufferedReadStream.Verify(v => v.DisposeAsync(), Times.Never());
}
private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType) private InputFormatterContext GetInputFormatterContext(byte[] contentBytes, Type modelType)
{ {
var httpContext = GetHttpContext(contentBytes); var httpContext = GetHttpContext(contentBytes);

View File

@ -129,6 +129,7 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
var suppressInputFormatterBuffering = _options.SuppressInputFormatterBuffering; var suppressInputFormatterBuffering = _options.SuppressInputFormatterBuffering;
var readStream = request.Body; var readStream = request.Body;
var disposeReadStream = false;
if (!request.Body.CanSeek && !suppressInputFormatterBuffering) if (!request.Body.CanSeek && !suppressInputFormatterBuffering)
{ {
// JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously // JSON.Net does synchronous reads. In order to avoid blocking on the stream, we asynchronously
@ -145,6 +146,8 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
await readStream.DrainAsync(CancellationToken.None); await readStream.DrainAsync(CancellationToken.None);
readStream.Seek(0L, SeekOrigin.Begin); readStream.Seek(0L, SeekOrigin.Begin);
disposeReadStream = true;
} }
var successful = true; var successful = true;
@ -170,9 +173,9 @@ namespace Microsoft.AspNetCore.Mvc.Formatters
jsonSerializer.Error -= ErrorHandler; jsonSerializer.Error -= ErrorHandler;
ReleaseJsonSerializer(jsonSerializer); ReleaseJsonSerializer(jsonSerializer);
if (readStream is FileBufferingReadStream fileBufferingReadStream) if (disposeReadStream)
{ {
await fileBufferingReadStream.DisposeAsync(); await readStream.DisposeAsync();
} }
} }
} }

View File

@ -23,6 +23,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="build\$(DefaultNetCoreTargetFramework)\*" Pack="true" PackagePath="build\$(DefaultNetCoreTargetFramework)" /> <None Include="targets\$(PackageId).targets" Pack="true" PackagePath="build\$(DefaultNetCoreTargetFramework)\$(PackageId).targets" />
<None Include="targets\$(PackageId).targets" Pack="true" PackagePath="buildTransitive\$(DefaultNetCoreTargetFramework)\$(PackageId).targets" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -105,6 +105,22 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
public CacheTagHelperOptions() { } public CacheTagHelperOptions() { }
public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } public long SizeLimit { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
} }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("component", Attributes="type", TagStructure=Microsoft.AspNetCore.Razor.TagHelpers.TagStructure.WithoutEndTag)]
public sealed partial class ComponentTagHelper : Microsoft.AspNetCore.Razor.TagHelpers.TagHelper
{
public ComponentTagHelper() { }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("type")]
public System.Type ComponentType { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("params", DictionaryAttributePrefix="param-")]
public System.Collections.Generic.IDictionary<string, object> Parameters { get { throw null; } set { } }
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNameAttribute("render-mode")]
public Microsoft.AspNetCore.Mvc.Rendering.RenderMode RenderMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[Microsoft.AspNetCore.Mvc.ViewFeatures.ViewContextAttribute]
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlAttributeNotBoundAttribute]
public Microsoft.AspNetCore.Mvc.Rendering.ViewContext ViewContext { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
[System.Diagnostics.DebuggerStepThroughAttribute]
public override System.Threading.Tasks.Task ProcessAsync(Microsoft.AspNetCore.Razor.TagHelpers.TagHelperContext context, Microsoft.AspNetCore.Razor.TagHelpers.TagHelperOutput output) { throw null; }
}
[Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")] [Microsoft.AspNetCore.Razor.TagHelpers.HtmlTargetElementAttribute("distributed-cache", Attributes="name")]
public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase public partial class DistributedCacheTagHelper : Microsoft.AspNetCore.Mvc.TagHelpers.CacheTagHelperBase
{ {

View File

@ -0,0 +1,80 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
/// <summary>
/// A <see cref="TagHelper"/> that renders a Razor component.
/// </summary>
[HtmlTargetElement("component", Attributes = ComponentTypeName, TagStructure = TagStructure.WithoutEndTag)]
public sealed class ComponentTagHelper : TagHelper
{
private const string ComponentParameterName = "params";
private const string ComponentParameterPrefix = "param-";
private const string ComponentTypeName = "type";
private const string RenderModeName = "render-mode";
private IDictionary<string, object> _parameters;
/// <summary>
/// Gets or sets the <see cref="Rendering.ViewContext"/> for the current request.
/// </summary>
[HtmlAttributeNotBound]
[ViewContext]
public ViewContext ViewContext { get; set; }
/// <summary>
/// Gets or sets values for component parameters.
/// </summary>
[HtmlAttributeName(ComponentParameterName, DictionaryAttributePrefix = ComponentParameterPrefix)]
public IDictionary<string, object> Parameters
{
get
{
_parameters ??= new Dictionary<string, object>(StringComparer.OrdinalIgnoreCase);
return _parameters;
}
set => _parameters = value;
}
/// <summary>
/// Gets or sets the component type. This value is required.
/// </summary>
[HtmlAttributeName(ComponentTypeName)]
public Type ComponentType { get; set; }
/// <summary>
/// Gets or sets the <see cref="Rendering.RenderMode"/>
/// </summary>
[HtmlAttributeName(RenderModeName)]
public RenderMode RenderMode { get; set; }
/// <inheritdoc />
public async override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (output == null)
{
throw new ArgumentNullException(nameof(output));
}
var componentRenderer = ViewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
var result = await componentRenderer.RenderComponentAsync(ViewContext, ComponentType, RenderMode, _parameters);
// Reset the TagName. We don't want `component` to render.
output.TagName = null;
output.Content.SetHtmlContent(result);
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO.Pipes;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
public class ComponentTagHelperTest
{
[Fact]
public async Task ProcessAsync_RendersComponent()
{
// Arrange
var tagHelper = new ComponentTagHelper
{
ViewContext = GetViewContext(),
};
var context = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act
await tagHelper.ProcessAsync(context, output);
// Assert
var content = HtmlContentUtilities.HtmlContentToString(output.Content);
Assert.Equal("Hello world", content);
Assert.Null(output.TagName);
}
private static TagHelperContext GetTagHelperContext()
{
return new TagHelperContext(
"component",
new TagHelperAttributeList(),
new Dictionary<object, object>(),
Guid.NewGuid().ToString("N"));
}
private static TagHelperOutput GetTagHelperOutput()
{
return new TagHelperOutput(
"component",
new TagHelperAttributeList(),
(_, __) => Task.FromResult<TagHelperContent>(new DefaultTagHelperContent()));
}
private ViewContext GetViewContext()
{
var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
var renderer = Mock.Of<IComponentRenderer>(c =>
c.RenderComponentAsync(It.IsAny<ViewContext>(), It.IsAny<Type>(), It.IsAny<RenderMode>(), It.IsAny<object>()) == Task.FromResult<IHtmlContent>(htmlContent));
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceCollection().AddSingleton<IComponentRenderer>(renderer).BuildServiceProvider(),
};
return new ViewContext
{
HttpContext = httpContext,
};
}
}
}

View File

@ -330,8 +330,8 @@ namespace Microsoft.AspNetCore.Mvc.Rendering
} }
public static partial class HtmlHelperComponentExtensions public static partial class HtmlHelperComponentExtensions
{ {
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, System.Type componentType, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) { throw null; }
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
[System.Diagnostics.DebuggerStepThroughAttribute]
public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; } public static System.Threading.Tasks.Task<Microsoft.AspNetCore.Html.IHtmlContent> RenderComponentAsync<TComponent>(this Microsoft.AspNetCore.Mvc.Rendering.IHtmlHelper htmlHelper, Microsoft.AspNetCore.Mvc.Rendering.RenderMode renderMode, object parameters) where TComponent : Microsoft.AspNetCore.Components.IComponent { throw null; }
} }
public static partial class HtmlHelperDisplayExtensions public static partial class HtmlHelperDisplayExtensions

View File

@ -18,7 +18,6 @@ using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers; using Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters; using Microsoft.AspNetCore.Mvc.ViewFeatures.Filters;
using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure; using Microsoft.AspNetCore.Mvc.ViewFeatures.Infrastructure;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Microsoft.JSInterop; using Microsoft.JSInterop;
@ -206,8 +205,9 @@ namespace Microsoft.Extensions.DependencyInjection
services.TryAddSingleton<SaveTempDataFilter>(); services.TryAddSingleton<SaveTempDataFilter>();
// //
// Component prerendering // Component rendering
// //
services.TryAddScoped<IComponentRenderer, ComponentRenderer>();
services.TryAddScoped<StaticComponentRenderer>(); services.TryAddScoped<StaticComponentRenderer>();
services.TryAddScoped<NavigationManager, HttpNavigationManager>(); services.TryAddScoped<NavigationManager, HttpNavigationManager>();
services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>(); services.TryAddScoped<IJSRuntime, UnsupportedJavaScriptRuntime>();

View File

@ -1,127 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentExtensions
{
private static readonly object ComponentSequenceKey = new object();
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
return htmlHelper.RenderComponentAsync<TComponent>(renderMode, null);
}
/// <summary>
/// Renders the <typeparamref name="TComponent"/> <see cref="IComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static async Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
RenderMode renderMode,
object parameters) where TComponent : IComponent
{
if (htmlHelper == null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
var context = htmlHelper.ViewContext.HttpContext;
return renderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(htmlHelper.ViewContext), typeof(TComponent), GetParametersCollection(parameters)),
RenderMode.Static => await StaticComponentAsync(context, typeof(TComponent), GetParametersCollection(parameters)),
_ => throw new ArgumentException("Invalid render mode", nameof(renderMode)),
};
}
private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
}
return (ServerComponentInvocationSequence)result;
}
private static ParameterView GetParametersCollection(object parameters) => parameters == null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
private static async Task<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(result);
}
private static async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var prerenderer = serviceProvider.GetRequiredService<StaticComponentRenderer>();
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await prerenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
invocationSerializer.GetPreamble(currentInvocation),
result,
invocationSerializer.GetEpilogue(currentInvocation));
}
private static IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var invocationSerializer = serviceProvider.GetRequiredService<ServerComponentSerializer>();
var currentInvocation = invocationSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(invocationSerializer.GetPreamble(currentInvocation));
}
}
}

View File

@ -0,0 +1,110 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal class ComponentRenderer : IComponentRenderer
{
private static readonly object ComponentSequenceKey = new object();
private readonly StaticComponentRenderer _staticComponentRenderer;
private readonly ServerComponentSerializer _serverComponentSerializer;
public ComponentRenderer(
StaticComponentRenderer staticComponentRenderer,
ServerComponentSerializer serverComponentSerializer)
{
_staticComponentRenderer = staticComponentRenderer;
_serverComponentSerializer = serverComponentSerializer;
}
public async Task<IHtmlContent> RenderComponentAsync(
ViewContext viewContext,
Type componentType,
RenderMode renderMode,
object parameters)
{
if (viewContext is null)
{
throw new ArgumentNullException(nameof(viewContext));
}
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException(Resources.FormatTypeMustDeriveFromType(componentType, typeof(IComponent)));
}
var context = viewContext.HttpContext;
var parameterView = parameters is null ?
ParameterView.Empty :
ParameterView.FromDictionary(HtmlHelper.ObjectToDictionary(parameters));
return renderMode switch
{
RenderMode.Server => NonPrerenderedServerComponent(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(context, GetOrCreateInvocationId(viewContext), componentType, parameterView),
RenderMode.Static => await StaticComponentAsync(context, componentType, parameterView),
_ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(renderMode), nameof(renderMode)),
};
}
private static ServerComponentInvocationSequence GetOrCreateInvocationId(ViewContext viewContext)
{
if (!viewContext.Items.TryGetValue(ComponentSequenceKey, out var result))
{
result = new ServerComponentInvocationSequence();
viewContext.Items[ComponentSequenceKey] = result;
}
return (ServerComponentInvocationSequence)result;
}
private async Task<IHtmlContent> StaticComponentAsync(HttpContext context, Type type, ParameterView parametersCollection)
{
var result = await _staticComponentRenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(result);
}
private async Task<IHtmlContent> PrerenderedServerComponentAsync(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var currentInvocation = _serverComponentSerializer.SerializeInvocation(
invocationId,
type,
parametersCollection,
prerendered: true);
var result = await _staticComponentRenderer.PrerenderComponentAsync(
parametersCollection,
context,
type);
return new ComponentHtmlContent(
_serverComponentSerializer.GetPreamble(currentInvocation),
result,
_serverComponentSerializer.GetEpilogue(currentInvocation));
}
private IHtmlContent NonPrerenderedServerComponent(HttpContext context, ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection)
{
var serviceProvider = context.RequestServices;
var currentInvocation = _serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false);
return new ComponentHtmlContent(_serverComponentSerializer.GetPreamble(currentInvocation));
}
}
}

View File

@ -0,0 +1,19 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{
internal interface IComponentRenderer
{
Task<IHtmlContent> RenderComponentAsync(
ViewContext viewContext,
Type componentType,
RenderMode renderMode,
object parameters);
}
}

View File

@ -11,11 +11,10 @@ using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing; using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Http.Extensions;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
internal class StaticComponentRenderer internal class StaticComponentRenderer
{ {

View File

@ -2,7 +2,6 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.JSInterop; using Microsoft.JSInterop;

View File

@ -0,0 +1,70 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
/// <summary>
/// Extensions for rendering components.
/// </summary>
public static class HtmlHelperComponentExtensions
{
/// <summary>
/// Renders the <typeparamref name="TComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(this IHtmlHelper htmlHelper, RenderMode renderMode) where TComponent : IComponent
=> RenderComponentAsync<TComponent>(htmlHelper, renderMode, parameters: null);
/// <summary>
/// Renders the <typeparamref name="TComponent"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
/// <returns>The HTML produced by the rendered <typeparamref name="TComponent"/>.</returns>
public static Task<IHtmlContent> RenderComponentAsync<TComponent>(
this IHtmlHelper htmlHelper,
RenderMode renderMode,
object parameters) where TComponent : IComponent
=> RenderComponentAsync(htmlHelper, typeof(TComponent), renderMode, parameters);
/// <summary>
/// Renders the specified <paramref name="componentType"/>.
/// </summary>
/// <param name="htmlHelper">The <see cref="IHtmlHelper"/>.</param>
/// <param name="componentType">The component type.</param>
/// <param name="parameters">An <see cref="object"/> containing the parameters to pass
/// to the component.</param>
/// <param name="renderMode">The <see cref="RenderMode"/> for the component.</param>
public static Task<IHtmlContent> RenderComponentAsync(
this IHtmlHelper htmlHelper,
Type componentType,
RenderMode renderMode,
object parameters)
{
if (htmlHelper is null)
{
throw new ArgumentNullException(nameof(htmlHelper));
}
if (componentType is null)
{
throw new ArgumentNullException(nameof(componentType));
}
var viewContext = htmlHelper.ViewContext;
var componentRenderer = viewContext.HttpContext.RequestServices.GetRequiredService<IComponentRenderer>();
return componentRenderer.RenderComponentAsync(viewContext, componentType, renderMode, parameters);
}
}
}

View File

@ -295,4 +295,7 @@
<data name="TempData_CannotDeserializeType" xml:space="preserve"> <data name="TempData_CannotDeserializeType" xml:space="preserve">
<value>Unsupported data type '{0}'.</value> <value>Unsupported data type '{0}'.</value>
</data> </data>
<data name="UnsupportedRenderMode" xml:space="preserve">
<value>Unsupported RenderMode '{0}'.</value>
</data>
</root> </root>

View File

@ -13,7 +13,7 @@ using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures.RazorComponents; using Microsoft.AspNetCore.Testing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
@ -22,24 +22,26 @@ using Microsoft.Net.Http.Headers;
using Moq; using Moq;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test namespace Microsoft.AspNetCore.Mvc.ViewFeatures
{ {
public class HtmlHelperComponentExtensionsTests public class ComponentRendererTest
{ {
private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$"; private const string PrerenderedServerComponentPattern = "^<!--Blazor:(?<preamble>.*?)-->(?<content>.+?)<!--Blazor:(?<epilogue>.*?)-->$";
private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$"; private const string ServerComponentPattern = "^<!--Blazor:(.*?)-->$";
private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider(); private static readonly IDataProtectionProvider _dataprotectorProvider = new EphemeralDataProtectionProvider();
private readonly ComponentRenderer renderer = GetComponentRenderer();
[Fact] [Fact]
public async Task CanRender_ParameterlessComponent() public async Task CanRender_ParameterlessComponent()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter(); var writer = new StringWriter();
// Act // Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Static); var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Static, null);
result.WriteTo(writer, HtmlEncoder.Default); result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString(); var content = writer.ToString();
@ -51,15 +53,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ParameterlessComponent_ServerMode() public async Task CanRender_ParameterlessComponent_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server); var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
result.WriteTo(writer, HtmlEncoder.Default); var content = HtmlContentUtilities.HtmlContentToString(result);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern); var match = Regex.Match(content, ServerComponentPattern);
// Assert // Assert
@ -82,15 +82,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanPrerender_ParameterlessComponent_ServerMode() public async Task CanPrerender_ParameterlessComponent_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered); var result = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
result.WriteTo(writer, HtmlEncoder.Default); var content = HtmlContentUtilities.HtmlContentToString(result);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert // Assert
@ -125,21 +123,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRenderMultipleServerComponents() public async Task CanRenderMultipleServerComponents()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var firstWriter = new StringWriter();
var secondWriter = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var firstResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.ServerPrerendered); var firstResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.ServerPrerendered, null);
firstResult.WriteTo(firstWriter, HtmlEncoder.Default); var firstComponent = HtmlContentUtilities.HtmlContentToString(firstResult);
var firstComponent = firstWriter.ToString();
var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline); var firstMatch = Regex.Match(firstComponent, PrerenderedServerComponentPattern, RegexOptions.Multiline);
var secondResult = await helper.RenderComponentAsync<TestComponent>(RenderMode.Server); var secondResult = await renderer.RenderComponentAsync(viewContext, typeof(TestComponent), RenderMode.Server, null);
secondResult.WriteTo(secondWriter, HtmlEncoder.Default); var secondComponent = HtmlContentUtilities.HtmlContentToString(secondResult);
var secondComponent = secondWriter.ToString();
var secondMatch = Regex.Match(secondComponent, ServerComponentPattern); var secondMatch = Regex.Match(secondComponent, ServerComponentPattern);
// Assert // Assert
@ -171,20 +165,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithParametersObject() public async Task CanRender_ComponentWithParametersObject()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Static, new { Name = "Steve" });
RenderMode.Static,
new
{
Name = "Steve"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
// Assert // Assert
var content = HtmlContentUtilities.HtmlContentToString(result);
Assert.Equal("<p>Hello Steve!</p>", content); Assert.Equal("<p>Hello Steve!</p>", content);
} }
@ -192,20 +179,13 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithParameters_ServerMode() public async Task CanRender_ComponentWithParameters_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = "Daniel" });
RenderMode.Server, var content = HtmlContentUtilities.HtmlContentToString(result);
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern); var match = Regex.Match(content, ServerComponentPattern);
// Assert // Assert
@ -237,20 +217,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_ComponentWithNullParameters_ServerMode() public async Task CanRender_ComponentWithNullParameters_ServerMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>(
RenderMode.Server, var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.Server, new { Name = (string)null });
new var content = HtmlContentUtilities.HtmlContentToString(result);
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, ServerComponentPattern); var match = Regex.Match(content, ServerComponentPattern);
// Assert // Assert
@ -274,28 +248,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
Assert.Null(parameterDefinition.TypeName); Assert.Null(parameterDefinition.TypeName);
Assert.Null(parameterDefinition.Assembly); Assert.Null(parameterDefinition.Assembly);
var value = Assert.Single(serverComponent.ParameterValues);; var value = Assert.Single(serverComponent.ParameterValues); ;
Assert.Null(value); Assert.Null(value);
} }
[Fact] [Fact]
public async Task CanPrerender_ComponentWithParameters_ServerMode() public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter(); var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = "Daniel" });
RenderMode.ServerPrerendered, var content = HtmlContentUtilities.HtmlContentToString(result);
new
{
Name = "Daniel"
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert // Assert
@ -336,23 +304,17 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
} }
[Fact] [Fact]
public async Task CanPrerender_ComponentWithNullParameters_ServerMode() public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter(); var writer = new StringWriter();
var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose)
.ToTimeLimitedDataProtector(); .ToTimeLimitedDataProtector();
// Act // Act
var result = await helper.RenderComponentAsync<GreetingComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, new { Name = (string)null });
RenderMode.ServerPrerendered, var content = HtmlContentUtilities.HtmlContentToString(result);
new
{
Name = (string)null
});
result.WriteTo(writer, HtmlEncoder.Default);
var content = writer.ToString();
var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline); var match = Regex.Match(content, PrerenderedServerComponentPattern, RegexOptions.Multiline);
// Assert // Assert
@ -396,39 +358,28 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task ComponentWithInvalidRenderMode_Throws() public async Task ComponentWithInvalidRenderMode_Throws()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
// Act & Assert // Act & Assert
var result = await Assert.ThrowsAsync<ArgumentException>(() => helper.RenderComponentAsync<GreetingComponent>( var ex = await ExceptionAssert.ThrowsArgumentAsync(
default, () => renderer.RenderComponentAsync(viewContext, typeof(GreetingComponent), default, new { Name = "Daniel" }),
new "renderMode",
{ $"Unsupported RenderMode '{(RenderMode)default}'");
Name = "Steve"
}));
Assert.Equal("renderMode", result.ParamName);
} }
[Fact] [Fact]
public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
// Act // Act
var state = new OnAfterRenderState(); var state = new OnAfterRenderState();
var result = await helper.RenderComponentAsync<OnAfterRenderComponent>( var result = await renderer.RenderComponentAsync(viewContext, typeof(OnAfterRenderComponent), RenderMode.Static, new { state });
RenderMode.Static,
new
{
State = state
});
result.WriteTo(writer, HtmlEncoder.Default);
// Assert // Assert
Assert.Equal("<p>Hello</p>", writer.ToString()); var content = HtmlContentUtilities.HtmlContentToString(result);
Assert.Equal("<p>Hello</p>", content);
Assert.False(state.OnAfterRenderRan); Assert.False(state.OnAfterRenderRan);
} }
@ -436,10 +387,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanCatch_ComponentWithSynchronousException() public async Task CanCatch_ComponentWithSynchronousException()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -454,10 +407,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanCatch_ComponentWithAsynchronousException() public async Task CanCatch_ComponentWithAsynchronousException()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -472,10 +427,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task Rendering_ComponentWithJsInteropThrows() public async Task Rendering_ComponentWithJsInteropThrows()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
// Act & Assert // Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<ExceptionComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(ExceptionComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -503,11 +460,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
var responseMock = new Mock<IHttpResponseFeature>(); var responseMock = new Mock<IHttpResponseFeature>();
responseMock.Setup(r => r.HasStarted).Returns(true); responseMock.Setup(r => r.HasStarted).Returns(true);
ctx.Features.Set(responseMock.Object); ctx.Features.Set(responseMock.Object);
var helper = CreateHelper(ctx); var viewContext = GetViewContext(ctx);
var writer = new StringWriter();
// Act // Act
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => helper.RenderComponentAsync<RedirectComponent>( var exception = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderComponentAsync(
viewContext,
typeof(RedirectComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -515,8 +473,8 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
})); }));
Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " + Assert.Equal("A navigation command was attempted during prerendering after the server already started sending the response. " +
"Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" + "Navigation commands can not be issued during server-side prerendering after the response from the server has started. Applications must buffer the" +
"reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.", "reponse and avoid using features like FlushAsync() before all components on the page have been rendered to prevent failed navigation commands.",
exception.Message); exception.Message);
} }
@ -530,10 +488,12 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
ctx.Request.PathBase = "/base"; ctx.Request.PathBase = "/base";
ctx.Request.Path = "/path"; ctx.Request.Path = "/path";
ctx.Request.QueryString = new QueryString("?query=value"); ctx.Request.QueryString = new QueryString("?query=value");
var helper = CreateHelper(ctx); var viewContext = GetViewContext(ctx);
// Act // Act
await helper.RenderComponentAsync<RedirectComponent>( await renderer.RenderComponentAsync(
viewContext,
typeof(RedirectComponent),
RenderMode.Static, RenderMode.Static,
new new
{ {
@ -549,8 +509,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
public async Task CanRender_AsyncComponent() public async Task CanRender_AsyncComponent()
{ {
// Arrange // Arrange
var helper = CreateHelper(); var viewContext = GetViewContext();
var writer = new StringWriter();
var expectedContent = @"<table> var expectedContent = @"<table>
<thead> <thead>
<tr> <tr>
@ -595,29 +554,29 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
</table>"; </table>";
// Act // Act
var result = await helper.RenderComponentAsync<AsyncComponent>(RenderMode.Static); var result = await renderer.RenderComponentAsync(viewContext,typeof(AsyncComponent), RenderMode.Static, null);
result.WriteTo(writer, HtmlEncoder.Default); var content = HtmlContentUtilities.HtmlContentToString(result);
var content = writer.ToString();
// Assert // Assert
Assert.Equal(expectedContent.Replace("\r\n", "\n"), content); Assert.Equal(expectedContent.Replace("\r\n", "\n"), content);
} }
private static IHtmlHelper CreateHelper(HttpContext ctx = null, Action<IServiceCollection> configureServices = null) private static ComponentRenderer GetComponentRenderer() =>
new ComponentRenderer(
new StaticComponentRenderer(HtmlEncoder.Default),
new ServerComponentSerializer(_dataprotectorProvider));
private static ViewContext GetViewContext(HttpContext context = null, Action<IServiceCollection> configureServices = null)
{ {
var services = new ServiceCollection(); var services = new ServiceCollection();
services.AddSingleton(HtmlEncoder.Default);
services.AddSingleton<ServerComponentSerializer>();
services.AddSingleton(_dataprotectorProvider); services.AddSingleton(_dataprotectorProvider);
services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>(); services.AddSingleton<IJSRuntime, UnsupportedJavaScriptRuntime>();
services.AddSingleton<NavigationManager, HttpNavigationManager>(); services.AddSingleton<NavigationManager, HttpNavigationManager>();
services.AddSingleton<StaticComponentRenderer>();
services.AddSingleton<ILoggerFactory, NullLoggerFactory>(); services.AddSingleton<ILoggerFactory, NullLoggerFactory>();
configureServices?.Invoke(services); configureServices?.Invoke(services);
var helper = new Mock<IHtmlHelper>(); context ??= new DefaultHttpContext();
var context = ctx ?? new DefaultHttpContext();
context.RequestServices = services.BuildServiceProvider(); context.RequestServices = services.BuildServiceProvider();
context.Request.Scheme = "http"; context.Request.Scheme = "http";
context.Request.Host = new HostString("localhost"); context.Request.Host = new HostString("localhost");
@ -625,12 +584,7 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Test
context.Request.Path = "/path"; context.Request.Path = "/path";
context.Request.QueryString = QueryString.FromUriComponent("?query=value"); context.Request.QueryString = QueryString.FromUriComponent("?query=value");
helper.Setup(h => h.ViewContext) return new ViewContext { HttpContext = context };
.Returns(new ViewContext()
{
HttpContext = context
});
return helper.Object;
} }
private class TestComponent : IComponent private class TestComponent : IComponent

View File

@ -6,13 +6,11 @@ using System.Collections.Generic;
using System.Runtime.ExceptionServices; using System.Runtime.ExceptionServices;
using System.Text.Encodings.Web; using System.Text.Encodings.Web;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Abstractions;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.RazorComponents namespace Microsoft.AspNetCore.Components.Rendering
{ {
public class HtmlRendererTest public class HtmlRendererTest
{ {

View File

@ -0,0 +1,56 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.Rendering
{
public class HtmlHelperComponentExtensionsTest
{
[Fact]
public async Task RenderComponentAsync_Works()
{
// Arrange
var viewContext = GetViewContext();
var htmlHelper = Mock.Of<IHtmlHelper>(h => h.ViewContext == viewContext);
// Act
var result = await HtmlHelperComponentExtensions.RenderComponentAsync<TestComponent>(htmlHelper, RenderMode.Static);
// Assert
Assert.Equal("Hello world", HtmlContentUtilities.HtmlContentToString(result));
}
private static ViewContext GetViewContext()
{
var htmlContent = new HtmlContentBuilder().AppendHtml("Hello world");
var renderer = Mock.Of<IComponentRenderer>(c =>
c.RenderComponentAsync(It.IsAny<ViewContext>(), It.IsAny<Type>(), It.IsAny<RenderMode>(), It.IsAny<object>()) == Task.FromResult<IHtmlContent>(htmlContent));
var httpContext = new DefaultHttpContext
{
RequestServices = new ServiceCollection().AddSingleton<IComponentRenderer>(renderer).BuildServiceProvider(),
};
var viewContext = new ViewContext { HttpContext = httpContext };
return viewContext;
}
private class TestComponent : IComponent
{
public void Attach(RenderHandle renderHandle)
{
}
public Task SetParametersAsync(ParameterView parameters) => null;
}
}
}

View File

@ -1,2 +1,13 @@
@using Microsoft.AspNetCore.Components.Routing @using Microsoft.AspNetCore.Components.Routing
<Router AppAssembly="@typeof(MvcSandbox.Startup).Assembly" FallbackComponent="@typeof(NotFound)" /> @using MvcSandbox.Components.Shared
<Router AppAssembly="@typeof(MvcSandbox.Startup).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="typeof(MainLayout)" />
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@ -1,4 +0,0 @@
@using MvcSandbox.Components.Shared
@layout MainLayout
<h1>Not Found</h1>
<h2>Sorry, nothing was found.</h2>

View File

@ -1,5 +1,4 @@
@page @page
@model MvcSandbox.Pages.ComponentsModel
@{ @{
Layout = null; Layout = null;
} }
@ -15,8 +14,7 @@
<link href="css/site.css" rel="stylesheet" /> <link href="css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
<app>@(await Html.RenderComponentAsync<MvcSandbox.Components.App>(RenderMode.Static))</app> <component type="typeof(MvcSandbox.Components.App)" render-mode="Static" />
<script src="_framework/blazor.server.js"></script>
<script src="_framework/components.server.js"></script>
</body> </body>
</html> </html>

View File

@ -1,16 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace MvcSandbox.Pages
{
public class ComponentsModel : PageModel
{
public void OnGet()
{
}
}
}

View File

@ -19,6 +19,9 @@
<li class="nav-item"> <li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a> <a class="nav-link text-dark" asp-area="" asp-page="/PagesHome">Pages Home</a>
</li> </li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-page="/Components">Components</a>
</li>
</ul> </ul>
</div> </div>
</div> </div>

View File

@ -2,7 +2,6 @@
<PropertyGroup> <PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework> <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<DefineConstants>$(DefineConstants)</DefineConstants>
<!-- We have tests that test runtime view compilation. --> <!-- We have tests that test runtime view compilation. -->
<RazorCompileOnBuild>false</RazorCompileOnBuild> <RazorCompileOnBuild>false</RazorCompileOnBuild>
<IsTestAssetProject>true</IsTestAssetProject> <IsTestAssetProject>true</IsTestAssetProject>
@ -21,4 +20,6 @@
<Reference Include="Microsoft.AspNetCore.Diagnostics" /> <Reference Include="Microsoft.AspNetCore.Diagnostics" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\Mvc.Razor.RuntimeCompilation\src\targets\Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation.targets" />
</Project> </Project>

View File

@ -11,7 +11,7 @@
<!--#if (IndividualLocalAuth && !UseLocalDB) --> <!--#if (IndividualLocalAuth && !UseLocalDB) -->
<ItemGroup> <ItemGroup>
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" /> <None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup> </ItemGroup>
<!--#endif --> <!--#endif -->

View File

@ -13,7 +13,7 @@
<!--#endif --> <!--#endif -->
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' "> <ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" /> <None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup> </ItemGroup>
<!--#if (IndividualB2CAuth || IndividualLocalAuth || OrganizationalAuth) --> <!--#if (IndividualB2CAuth || IndividualLocalAuth || OrganizationalAuth) -->

View File

@ -13,7 +13,7 @@
<!--#endif --> <!--#endif -->
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' "> <ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" /> <None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup> </ItemGroup>
<!--#if (IndividualB2CAuth || IndividualLocalAuth || OrganizationalAuth) --> <!--#if (IndividualB2CAuth || IndividualLocalAuth || OrganizationalAuth) -->

View File

@ -14,9 +14,20 @@
</head> </head>
<body> <body>
<app> <app>
@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered)) <component type="typeof(App)" render-mode="ServerPrerendered" />
</app> </app>
<div id="blazor-error-ui">
<environment include="Staging,Production">
An error has occurred. This application may no longer respond until reloaded.
</environment>
<environment include="Development">
An unhandled exception has occurred. See browser dev tools for details.
</environment>
<a href class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.server.js"></script> <script src="_framework/blazor.server.js"></script>
</body> </body>
</html> </html>

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -111,6 +111,25 @@ app {
color: red; color: red;
} }
#blazor-error-ui {
background: lightyellow;
bottom: 0;
box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
display: none;
left: 0;
padding: 0.6rem 1.25rem 0.7rem 1.25rem;
position: fixed;
width: 100%;
z-index: 1000;
}
#blazor-error-ui .dismiss {
cursor: pointer;
position: absolute;
right: 0.75rem;
top: 0.5rem;
}
@media (max-width: 767.98px) { @media (max-width: 767.98px) {
.main .top-row { .main .top-row {
display: none; display: none;

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -1,7 +1,8 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Warning", "Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
}, },

View File

@ -2,7 +2,7 @@ syntax = "proto3";
option csharp_namespace = "GrpcService_CSharp"; option csharp_namespace = "GrpcService_CSharp";
package Greet; package greet;
// The greeting service definition. // The greeting service definition.
service Greeter { service Greeter {

View File

@ -1,7 +1,8 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Warning", "Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
}, },

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -1,7 +1,8 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Warning", "Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
}, },

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -1,7 +1,8 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Warning", "Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
}, },

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
} }
} }

View File

@ -27,7 +27,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' "> <ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" /> <None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list --> <!-- Don't publish the SPA source files, but do show them in the project files list -->

View File

@ -24,7 +24,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' "> <ItemGroup Condition=" '$(IndividualLocalAuth)' == 'True' AND '$(UseLocalDB)' != 'True' ">
<None Update="app.db" CopyToOutputDirectory="PreserveNewest" /> <None Update="app.db" CopyToOutputDirectory="PreserveNewest" ExcludeFromSingleFile="true" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<!-- Don't publish the SPA source files, but do show them in the project files list --> <!-- Don't publish the SPA source files, but do show them in the project files list -->

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
////#if (IndividualLocalAuth) ////#if (IndividualLocalAuth)
// }, // },

View File

@ -10,7 +10,9 @@
////#endif ////#endif
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Warning" "Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
} }
}, },
////#if (IndividualLocalAuth) ////#if (IndividualLocalAuth)

View File

@ -1,9 +1,9 @@
{ {
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Debug", "Default": "Information",
"System": "Information", "Microsoft": "Warning",
"Microsoft": "Information" "Microsoft.Hosting.Lifetime": "Information"
} }
////#if (IndividualLocalAuth) ////#if (IndividualLocalAuth)
// }, // },

View File

@ -10,7 +10,9 @@
////#endif ////#endif
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"Default": "Warning" "Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
} }
}, },
////#if (IndividualLocalAuth) ////#if (IndividualLocalAuth)

Some files were not shown because too many files have changed in this diff Show More