Skip to content

Commit a082b18

Browse files
authored
Fix WebView header APIs and behavior (#31246)
* Fix WebView header APIs and behavior - Change WebView to return dictionary of headers instead of strings (this is needed for Android) - Change StaticContentProvider to use filename (always exists) instead of physical file path (not always available) to determine MIME type - Fix WPF sample bug
1 parent 11dd699 commit a082b18

File tree

6 files changed

+159
-8
lines changed

6 files changed

+159
-8
lines changed

src/Components/WebView/Platforms/WebView2/src/WebView2WebViewManager.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
6+
using System.Linq;
57
using System.Threading.Tasks;
68
using Microsoft.Extensions.FileProviders;
79
using Microsoft.Web.WebView2.Core;
@@ -73,7 +75,8 @@ private async Task InitializeWebView2()
7375

7476
if (TryGetResponseContent(eventArgs.Request.Uri, allowFallbackOnHostPage, out var statusCode, out var statusMessage, out var content, out var headers))
7577
{
76-
eventArgs.Response = environment.CreateWebResourceResponse(content, statusCode, statusMessage, headers);
78+
var headerString = GetHeaderString(headers);
79+
eventArgs.Response = environment.CreateWebResourceResponse(content, statusCode, statusMessage, headerString);
7780
}
7881
};
7982

@@ -94,6 +97,9 @@ await _webview.CoreWebView2.AddScriptToExecuteOnDocumentCreatedAsync(@"
9497
=> MessageReceived(new Uri(eventArgs.Source), eventArgs.TryGetWebMessageAsString());
9598
}
9699

100+
private static string GetHeaderString(IDictionary<string, string> headers) =>
101+
string.Join(Environment.NewLine, headers.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
102+
97103
private void ApplyDefaultWebViewSettings()
98104
{
99105
// Desktop applications typically don't want the default web browser context menu

src/Components/WebView/Samples/BlazorWpfApp/Pages/Index.razor

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
@page "/"
22
@using WebviewAppShared
3-
// NOTE: The full namespace is included here to work around this bug: https://github.com/dotnet/aspnetcore/issues/30851
3+
@* NOTE: The full namespace is included here to work around this bug: https://github.com/dotnet/aspnetcore/issues/30851 *@
44
@inject BlazorWpfApp.AppState AppState
55

66
<h3>Hello, world!</h3>

src/Components/WebView/WebView/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Microsoft.AspNetCore.Components.WebView.WebViewManager.Dispose() -> void
66
Microsoft.AspNetCore.Components.WebView.WebViewManager.MessageReceived(System.Uri! sourceUri, string! message) -> void
77
Microsoft.AspNetCore.Components.WebView.WebViewManager.Navigate(string! url) -> void
88
Microsoft.AspNetCore.Components.WebView.WebViewManager.RemoveRootComponentAsync(string! selector) -> System.Threading.Tasks.Task!
9-
Microsoft.AspNetCore.Components.WebView.WebViewManager.TryGetResponseContent(string! uri, bool allowFallbackOnHostPage, out int statusCode, out string! statusMessage, out System.IO.Stream! content, out string! headers) -> bool
9+
Microsoft.AspNetCore.Components.WebView.WebViewManager.TryGetResponseContent(string! uri, bool allowFallbackOnHostPage, out int statusCode, out string! statusMessage, out System.IO.Stream! content, out System.Collections.Generic.IDictionary<string!, string!>! headers) -> bool
1010
Microsoft.AspNetCore.Components.WebView.WebViewManager.WebViewManager(System.IServiceProvider! provider, Microsoft.AspNetCore.Components.Dispatcher! dispatcher, System.Uri! appBaseUri, Microsoft.Extensions.FileProviders.IFileProvider! fileProvider, string! hostPageRelativePath) -> void
1111
Microsoft.Extensions.DependencyInjection.ComponentsWebViewServiceCollectionExtensions
1212
abstract Microsoft.AspNetCore.Components.WebView.WebViewManager.NavigateCore(System.Uri! absoluteUri) -> void

src/Components/WebView/WebView/src/StaticContentProvider.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
5+
using System.Collections.Generic;
56
using System.IO;
67
using System.Text;
78
using Microsoft.Extensions.FileProviders;
@@ -24,7 +25,7 @@ public StaticContentProvider(IFileProvider fileProvider, Uri appBaseUri, string
2425
_hostPageRelativePath = hostPageRelativePath ?? throw new ArgumentNullException(nameof(hostPageRelativePath));
2526
}
2627

27-
public bool TryGetResponseContent(string requestUri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out string headers)
28+
public bool TryGetResponseContent(string requestUri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary<string, string> headers)
2829
{
2930
var fileUri = new Uri(requestUri);
3031
if (_appBaseUri.IsBaseOf(fileUri))
@@ -75,7 +76,7 @@ private bool TryGetFromFileProvider(string relativePath, out Stream content, out
7576
if (fileInfo.Exists)
7677
{
7778
content = fileInfo.CreateReadStream();
78-
contentType = GetResponseContentTypeOrDefault(fileInfo.PhysicalPath);
79+
contentType = GetResponseContentTypeOrDefault(fileInfo.Name);
7980
return true;
8081
}
8182
}
@@ -107,7 +108,11 @@ private static string GetResponseContentTypeOrDefault(string path)
107108
? matchedContentType
108109
: "application/octet-stream";
109110

110-
private static string GetResponseHeaders(string contentType)
111-
=> $"Content-Type: {contentType}{Environment.NewLine}Cache-Control: no-cache, max-age=0, must-revalidate, no-store";
111+
private static IDictionary<string, string> GetResponseHeaders(string contentType)
112+
=> new Dictionary<string, string>()
113+
{
114+
{ "Content-Type", contentType },
115+
{ "Cache-Control", "no-cache, max-age=0, must-revalidate, no-store" },
116+
};
112117
}
113118
}

src/Components/WebView/WebView/src/WebViewManager.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ protected void MessageReceived(Uri sourceUri, string message)
168168
/// <param name="content">The response content</param>
169169
/// <param name="headers">The response headers</param>
170170
/// <returns><c>true</c> if the response can be provided; <c>false</c> otherwise.</returns>
171-
protected bool TryGetResponseContent(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out string headers)
171+
protected bool TryGetResponseContent(string uri, bool allowFallbackOnHostPage, out int statusCode, out string statusMessage, out Stream content, out IDictionary<string, string> headers)
172172
=> _staticContentProvider.TryGetResponseContent(uri, allowFallbackOnHostPage, out statusCode, out statusMessage, out content, out headers);
173173

174174
internal async Task AttachToPageAsync(string baseUrl, string startUrl)
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.IO;
7+
using System.Linq;
8+
using System.Text;
9+
using Microsoft.Extensions.FileProviders;
10+
using Microsoft.Extensions.Primitives;
11+
using Xunit;
12+
13+
namespace Microsoft.AspNetCore.Components.WebView
14+
{
15+
public class StaticContentProviderTests
16+
{
17+
[Fact]
18+
public void TryGetResponseContentReturnsCorrectContentTypeForNonPhysicalFile()
19+
{
20+
// Arrange
21+
const string cssFilePath = "folder/file.css";
22+
const string cssFileContent = "this is css";
23+
var inMemoryFileProvider = new InMemoryFileProvider(
24+
new Dictionary<string, string>
25+
{
26+
{ cssFilePath, cssFileContent },
27+
});
28+
var appBase = "fake://0.0.0.0/";
29+
var scp = new StaticContentProvider(inMemoryFileProvider, new Uri(appBase), "fakehost.html");
30+
31+
// Act
32+
Assert.True(scp.TryGetResponseContent(
33+
requestUri: appBase + cssFilePath,
34+
allowFallbackOnHostPage: false,
35+
out var statusCode,
36+
out var statusMessage,
37+
out var content,
38+
out var headers));
39+
40+
// Assert
41+
var contentString = new StreamReader(content).ReadToEnd();
42+
Assert.Equal(200, statusCode);
43+
Assert.Equal("OK", statusMessage);
44+
Assert.Equal("this is css", contentString);
45+
Assert.True(headers.TryGetValue("Content-Type", out var contentTypeValue));
46+
Assert.Equal("text/css", contentTypeValue);
47+
}
48+
49+
private sealed class InMemoryFileProvider : IFileProvider
50+
{
51+
public InMemoryFileProvider(IDictionary<string, string> filePathsAndContents)
52+
{
53+
if (filePathsAndContents is null)
54+
{
55+
throw new ArgumentNullException(nameof(filePathsAndContents));
56+
}
57+
58+
FilePathsAndContents = filePathsAndContents;
59+
}
60+
61+
public IDictionary<string, string> FilePathsAndContents { get; }
62+
63+
public IDirectoryContents GetDirectoryContents(string subpath)
64+
{
65+
return new InMemoryDirectoryContents(this, subpath);
66+
}
67+
68+
public IFileInfo GetFileInfo(string subpath)
69+
{
70+
return FilePathsAndContents
71+
.Where(kvp => kvp.Key == subpath)
72+
.Select(x => new InMemoryFileInfo(x.Key, x.Value))
73+
.Single();
74+
}
75+
76+
public IChangeToken Watch(string filter)
77+
{
78+
return null;
79+
}
80+
81+
private sealed class InMemoryDirectoryContents : IDirectoryContents
82+
{
83+
private readonly InMemoryFileProvider _inMemoryFileProvider;
84+
private readonly string _subPath;
85+
86+
public InMemoryDirectoryContents(InMemoryFileProvider inMemoryFileProvider, string subPath)
87+
{
88+
_inMemoryFileProvider = inMemoryFileProvider ?? throw new ArgumentNullException(nameof(inMemoryFileProvider));
89+
_subPath = subPath ?? throw new ArgumentNullException(nameof(inMemoryFileProvider));
90+
}
91+
92+
public bool Exists => true;
93+
94+
public IEnumerator<IFileInfo> GetEnumerator()
95+
{
96+
return
97+
_inMemoryFileProvider
98+
.FilePathsAndContents
99+
.Where(kvp => kvp.Key.StartsWith(_subPath, StringComparison.Ordinal))
100+
.Select(x => new InMemoryFileInfo(x.Key, x.Value))
101+
.GetEnumerator();
102+
}
103+
104+
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
105+
{
106+
return GetEnumerator();
107+
}
108+
}
109+
110+
private sealed class InMemoryFileInfo : IFileInfo
111+
{
112+
private readonly string _filePath;
113+
private readonly string _fileContents;
114+
115+
public InMemoryFileInfo(string filePath, string fileContents)
116+
{
117+
_filePath = filePath;
118+
_fileContents = fileContents;
119+
}
120+
121+
public bool Exists => true;
122+
123+
public long Length => _fileContents.Length;
124+
125+
public string PhysicalPath => null;
126+
127+
public string Name => Path.GetFileName(_filePath);
128+
129+
public DateTimeOffset LastModified => DateTimeOffset.Now;
130+
131+
public bool IsDirectory => false;
132+
133+
public Stream CreateReadStream()
134+
{
135+
return new MemoryStream(Encoding.UTF8.GetBytes(_fileContents));
136+
}
137+
}
138+
}
139+
}
140+
}

0 commit comments

Comments
 (0)