Проблема MAUI Hybrid с запросом HTTP GET от BlazorWebViewC#

Место общения программистов C#
Anonymous
Проблема MAUI Hybrid с запросом HTTP GET от BlazorWebView

Сообщение Anonymous »

Я работаю над гибридным решением MAUI со стандартными проектами: Интернет (Сервер) / Общий / Мобильный. Моя проблема заключается в том, что я запускаю приложение на эмуляторе Android.
Вот мой файл MainActivity.cs:

Код: Выделить всё

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}
Вот мой MainApplication.cs:

Код: Выделить всё

[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}

protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}
Вот мой PermissionManagingBlazorWebChromeClient.cs (настроенный, поскольку мне нужно получить разрешение от пользователя на использование API геолокации:

Код: Выделить всё

internal class PermissionManagingBlazorWebChromeClient : WebChromeClient, IActivityResultCallback
{
// This class implements a permission requesting workflow that matches workflow recommended
// by the official Android developer documentation.
// See: https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions
// The current implementation supports location, camera, and microphone permissions. To add your own,
// update the s_rationalesByPermission dictionary to include your rationale for requiring the permission.
// If necessary, you may need to also update s_requiredPermissionsByWebkitResource to define how a specific
// Webkit resource maps to an Android permission.

// In a real app, you would probably use more convincing rationales tailored toward what your app does.
private const string CameraAccessRationale = "This app requires access to your camera. Please grant access to your camera when requested.";
private const string LocationAccessRationale = "This app requires access to your location. Please grant access to your precise location when requested.";
private const string MicrophoneAccessRationale = "This app requires access to your microphone.  Please grant access to your microphone when requested.";

private static readonly Dictionary s_rationalesByPermission = new()
{
[Manifest.Permission.Camera] = CameraAccessRationale,
[Manifest.Permission.AccessFineLocation] = LocationAccessRationale,
[Manifest.Permission.RecordAudio] = MicrophoneAccessRationale,
// Add more rationales as you add more supported permissions.
};

private static readonly Dictionary s_requiredPermissionsByWebkitResource = new()
{
[PermissionRequest.ResourceVideoCapture] = new[] { Manifest.Permission.Camera },
[PermissionRequest.ResourceAudioCapture] = new[] { Manifest.Permission.ModifyAudioSettings, Manifest.Permission.RecordAudio },
// Add more Webkit resource -> Android permission mappings as needed.
};

private readonly WebChromeClient _blazorWebChromeClient;
private readonly ComponentActivity _activity;
private readonly ActivityResultLauncher _requestPermissionLauncher;

private Action? _pendingPermissionRequestCallback;

public PermissionManagingBlazorWebChromeClient(WebChromeClient blazorWebChromeClient, ComponentActivity activity)
{
_blazorWebChromeClient = blazorWebChromeClient;
_activity = activity;
_requestPermissionLauncher = _activity.RegisterForActivityResult(new ActivityResultContracts.RequestPermission(), this);
}

public override void OnCloseWindow(WebView? window)
{
_blazorWebChromeClient.OnCloseWindow(window);
_requestPermissionLauncher.Unregister();
}

public override void OnGeolocationPermissionsShowPrompt(string? origin, GeolocationPermissions.ICallback? callback)
{
ArgumentNullException.ThrowIfNull(callback, nameof(callback));

RequestPermission(Manifest.Permission.AccessFineLocation, isGranted => callback.Invoke(origin, isGranted, false));
}

public override void OnPermissionRequest(PermissionRequest? request)
{
ArgumentNullException.ThrowIfNull(request, nameof(request));

if (request.GetResources() is not { } requestedResources)
{
request.Deny();
return;
}

RequestAllResources(requestedResources, grantedResources =>
{
if (grantedResources.Count == 0)
{
request.Deny();
}
else
{
request.Grant(grantedResources.ToArray());
}
});
}

private void RequestAllResources(Memory requestedResources, Action callback)
{
if (requestedResources.Length == 0)
{
// No resources to request - invoke the callback with an empty list.
callback(new());
return;
}

var currentResource = requestedResources.Span[0];
var requiredPermissions = s_requiredPermissionsByWebkitResource.GetValueOrDefault(currentResource, Array.Empty());

RequestAllPermissions(requiredPermissions, isGranted =>
{
// Recurse with the remaining resources. If the first resource was granted, use a modified callback
// that adds the first resource to the granted resources list.
RequestAllResources(requestedResources[1..], !isGranted ? callback : grantedResources =>
{
grantedResources.Add(currentResource);
callback(grantedResources);
});
});
}

private void RequestAllPermissions(Memory requiredPermissions, Action callback)
{
if (requiredPermissions.Length == 0)
{
// No permissions left to request - success!
callback(true);
return;
}

RequestPermission(requiredPermissions.Span[0], isGranted =>
{
if (isGranted)
{
// Recurse with the remaining permissions.
RequestAllPermissions(requiredPermissions[1..], callback);
}
else
{
// The first required permission was not granted.  Fail now and don't attempt to grant
// the remaining permissions.
callback(false);
}
});
}

private void RequestPermission(string permission, Action callback)
{
// This method implements the workflow described here:
// https://developer.android.com/training/permissions/requesting#workflow_for_requesting_permissions

if (ContextCompat.CheckSelfPermission(_activity, permission) == Permission.Granted)
{
callback.Invoke(true);
}
else if (_activity.ShouldShowRequestPermissionRationale(permission) && s_rationalesByPermission.TryGetValue(permission, out var rationale))
{
new AlertDialog.Builder(_activity)
.SetTitle("Enable app permissions")!
.SetMessage(rationale)!
.SetNegativeButton("No thanks", (_, _) => callback(false))!
.SetPositiveButton("Continue", (_, _) => LaunchPermissionRequestActivity(permission, callback))!
.Show();
}
else
{
LaunchPermissionRequestActivity(permission, callback);
}
}

private void LaunchPermissionRequestActivity(string permission, Action callback)
{
if (_pendingPermissionRequestCallback is not null)
{
throw new InvalidOperationException("Cannot perform multiple permission requests simultaneously.");
}

_pendingPermissionRequestCallback = callback;
_requestPermissionLauncher.Launch(permission);
}

void IActivityResultCallback.OnActivityResult(Java.Lang.Object isGranted)
{
var callback = _pendingPermissionRequestCallback;
_pendingPermissionRequestCallback = null;
callback?.Invoke((bool)isGranted);
}

#region Unremarkable overrides
// See: https://github.com/dotnet/maui/issues/6565
public override JniPeerMembers JniPeerMembers => _blazorWebChromeClient.JniPeerMembers;
public override Bitmap? DefaultVideoPoster => _blazorWebChromeClient.DefaultVideoPoster;
public override View? VideoLoadingProgressView => _blazorWebChromeClient.VideoLoadingProgressView;
public override void GetVisitedHistory(IValueCallback? callback)
=> _blazorWebChromeClient.GetVisitedHistory(callback);
public override bool OnConsoleMessage(ConsoleMessage? consoleMessage)
=> _blazorWebChromeClient.OnConsoleMessage(consoleMessage);
public override bool OnCreateWindow(WebView? view, bool isDialog, bool isUserGesture, Message? resultMsg)
=> _blazorWebChromeClient.OnCreateWindow(view, isDialog, isUserGesture, resultMsg);
public override void OnGeolocationPermissionsHidePrompt()
=> _blazorWebChromeClient.OnGeolocationPermissionsHidePrompt();
public override void OnHideCustomView()
=> _blazorWebChromeClient.OnHideCustomView();
public override bool OnJsAlert(WebView? view, string? url, string? message, JsResult? result)
=> _blazorWebChromeClient.OnJsAlert(view, url, message, result);
public override bool OnJsBeforeUnload(WebView? view, string? url, string? message, JsResult? result)
=> _blazorWebChromeClient.OnJsBeforeUnload(view, url, message, result);
public override bool OnJsConfirm(WebView? view, string? url, string? message, JsResult? result)
=> _blazorWebChromeClient.OnJsConfirm(view, url, message, result);
public override bool OnJsPrompt(WebView? view, string? url, string? message, string? defaultValue, JsPromptResult? result)
=> _blazorWebChromeClient.OnJsPrompt(view, url, message, defaultValue, result);
public override void OnPermissionRequestCanceled(PermissionRequest? request)
=> _blazorWebChromeClient.OnPermissionRequestCanceled(request);
public override void OnProgressChanged(WebView? view, int newProgress)
=> _blazorWebChromeClient.OnProgressChanged(view, newProgress);
public override void OnReceivedIcon(WebView? view, Bitmap? icon)
=> _blazorWebChromeClient.OnReceivedIcon(view, icon);
public override void OnReceivedTitle(WebView? view, string? title)
=> _blazorWebChromeClient.OnReceivedTitle(view, title);
public override void OnReceivedTouchIconUrl(WebView? view, string? url, bool precomposed)
=>  _blazorWebChromeClient.OnReceivedTouchIconUrl(view, url, precomposed);
public override void OnRequestFocus(WebView? view)
=> _blazorWebChromeClient.OnRequestFocus(view);
public override void OnShowCustomView(View? view, ICustomViewCallback? callback)
=> _blazorWebChromeClient.OnShowCustomView(view, callback);
public override bool OnShowFileChooser(WebView? webView, IValueCallback? filePathCallback, FileChooserParams? fileChooserParams)
=> _blazorWebChromeClient.OnShowFileChooser(webView, filePathCallback, fileChooserParams);
#endregion
}
Вот мой MainPage.xaml: Вот мой MainPage.xaml.cs:

Код: Выделить всё

public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();

blazorWebView.BlazorWebViewInitializing += BlazorWebViewInitializing;
blazorWebView.BlazorWebViewInitialized += BlazorWebViewInitialized;
}

private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e);
private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e);
}
Вот мой MainPage.xaml.Android.cs:

Код: Выделить всё

public partial class MainPage
{
// To manage Android permissions, update AndroidManifest.xml to include the permissions and
// features required by your app. You may have to perform additional configuration to enable
// use of those APIs from the WebView, as is done below. A custom WebChromeClient is needed
// to define what happens when the WebView requests a set of permissions.  See
// PermissionManagingBlazorWebChromeClient.cs to explore the approach taken in this example.

private partial void BlazorWebViewInitializing(object? sender, BlazorWebViewInitializingEventArgs e)
{
}

private partial void BlazorWebViewInitialized(object? sender, BlazorWebViewInitializedEventArgs e)
{
if (e.WebView.Context?.GetActivity() is not ComponentActivity activity)
{
throw new InvalidOperationException($"The permission-managing WebChromeClient requires that the current activity be a '{nameof(ComponentActivity)}'.");
}

e.WebView.Settings.JavaScriptEnabled = true;
e.WebView.Settings.AllowContentAccess = true;
e.WebView.Settings.AllowFileAccess = true;
e.WebView.Settings.AllowFileAccessFromFileURLs = true;
e.WebView.Settings.AllowUniversalAccessFromFileURLs = true;
e.WebView.Settings.DomStorageEnabled = true;
e.WebView.Settings.DatabaseEnabled = true;
e.WebView.Settings.MediaPlaybackRequiresUserGesture = false;
e.WebView.Settings.SetGeolocationEnabled(true);
e.WebView.Settings.SetGeolocationDatabasePath(e.WebView.Context?.FilesDir?.Path);
e.WebView.SetWebChromeClient(new PermissionManagingBlazorWebChromeClient(e.WebView.WebChromeClient!, activity));
}
}
Я добавил следующее в csproj для проекта Mobile: Мой API очень прост в файле Web (Server) Program.cs:

Код: Выделить всё

var api = app.MapGroup("/api");

api.MapGet("/documents", async ([FromServices] IMainDataService data, [FromServices] HttpContextAccessor context, [FromServices] UserManager userManager, int range, int selectedItemId) =>
{
var results = await data.GetAllDocumentsInRangeAsync(range, selectedItemId);

//context.HttpContext.Response.Headers.AccessControlAllowOrigin = "*";

return Results.Ok(results);
}).RequireAuthorization();
Как видите, я даже пытался установить AccessControlAllowOrigin самостоятельно, но это не меняет поведение, описанное ниже.
В проекте Shared у меня есть общая страница бритвы «Home.razor», из которой у меня есть фрагмент кода jQuery для получения некоторых данных из веб-проекта.
Это простой $.get следующим образом:

Код: Выделить всё

var requestData = [];

$.get(baseUrl + '/api/documents', {
"range": distance,
"selectedItemId": selectedItemId
})
.done(function (data) {
console.log("DOCUMENTS RECEIVED");

$.each(data, function (index, value) {

var reqData = {
name: value.name,
displayIndex: value.displayIndex,
itemId: value.id
};
requestData.push(reqData);

console.log("Added " + reqData.name + " to list of documents...");
});
});
Когда я запускаю веб-проект (сервер), выполняется HTTP-запрос GET и загружаются данные, как ожидалось.
Но когда я запускаю его из мобильного проекта (Android), я получаю странное поведение. Первоначально говорилось, что есть проблемы с CORS. Что я решил с помощью AddCors() в веб-проекте/Program.cs:

Код: Выделить всё

builder.Services.AddCors(options =>
{
options.AddDefaultPolicy(
policy =>
{
policy.AllowAnyHeader();
policy.SetIsOriginAllowedToAllowWildcardSubdomains();
policy.AllowAnyMethod();
policy.AllowAnyOrigin();
policy.SetPreflightMaxAge(TimeSpan.FromSeconds(3600));
});
});

...

app.UseCors();
Когда я запускаю приложение в эмуляторе Android, кажется, что запрос HTTP GET не полностью обработан. Вот что я получаю из трассировки:

Код: Выделить всё

Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler: Debug: Intercepting request for https://localhost:7087/api/documents?range=10&selectedItemId=1.
Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler: Debug: Handling web request to URI 'https://localhost:7087/api/documents'.
Microsoft.AspNetCore.Components.WebView.Maui.BlazorWebViewHandler: Debug: Response content was not found for web request to URI 'https://localhost:7087/api/documents'.
И вот что я получаю в DevTools «Отображаются предварительные заголовки»:
Изображение

Нет ни кода состояния HTTP, ни заголовков ответа.
В политике CORS я пытался указать Origins или использовать метод AllowAnyOrigin(), я получаю тот же результат.
Я использую VS2026, .NET 10 и jQuery 4.0.0 мин: Может быть, мне не хватает настройки BlazorWebView? Или что-то еще?

Вернуться в «C#»