【问题标题】:C#/Xamarin HttpClientSendAsync not returning when waiting for to longC#/Xamarin HttpClientSendAsync 在等待很长时间时不返回
【发布时间】:2021-08-12 10:18:59
【问题描述】:

最近我在移动设备上调用 API 端点时遇到了几个问题。我的模拟器从来没有遇到过这个问题(因为网络更好),但我终于可以重现它了。

所以会发生以下情况: 用户启动他们的应用程序并调用多个 API 端点来检索数据。然而,其中一些 API 端点需要很长时间。

所以API调用设置如下:

public async Task<bool> GetData()
{
         
    Task.Run(async () =>
    {
        if (App.elementsSynced == false)
            await _oDataService.GetElements(_userId, _token);
            
        // .... more code
    }
}

所有调用 GetData() 的(顶级)方法也是异步的,等待调用。

GetElements():

public async Task GetElements(string userId, string token)
{
    if (App.elementsSynced == true)
    {
        return;
    }

    try
    {

        var apiEndpointUri = new Uri(_apiUri, $"GetElements(UserId='{userId}')");
        var httpRequestMessage = new HttpRequestMessage(HttpMethod.Get, apiEndpointUri);
        httpRequestMessage.Headers.Add("Authorization", $"Bearer {token}");
        var response = await _httpClient.SendAsync(httpRequestMessage);
        HttpStatusCode statusCode = response.StatusCode;

        var responseResult = await response.Content.ReadAsStringAsync();

        if (statusCode != HttpStatusCode.OK)
        {
            // ... code omitted

            return;
        }


        // .. saving data locally

    
        App.elementsSynced = true;
        return;

    }
    catch (Exception ex)
    {
        
    }
}

当有稳定的互联网连接时,上面的代码可以正常工作,但是当它需要很长时间并且我不知道确切的阈值是多少时,那么正在调用的结果是在

var response = await _httpClient.SendAsync(httpRequestMessage);

永远不会被返回,当数据在大约 60 秒后返回时也不会被返回(用 60 秒的阈值对其进行测试)。

我通过在我的 API 端点中设置断点并等待大约 60 秒左右来重现此问题。当我在 60 秒后恢复 API 并且正在发回数据时,它永远不会到达调用者(Xamarin APP)。

是否存在 http 请求不再返回结果的“隐藏”超时?我知道.NET 5 引入了TimeoutException,我可以在HttpClient 中设置超时时使用它,但我不知道(最大)超时应该是多少。默认值是 100 秒,所以我希望在 100 秒后它无论如何都应该属于异常,但它不会..

【问题讨论】:

  • 您正在使用 Task.Run() 在自己的任务中调用 GetElements。我不会这样做,GetElements 是等待的。因此,您可以在 GetData 中调用 await GetElements() 并尝试捕获异常。
  • 我也对这样做有所保留,但最后我希望在其余逻辑(和 UI)继续运行的同时运行“同步”方法。现在我在启动方法 (IntializeAsync) 中调用 GetData(),如果我要删除 Task.Run,那么所有 API 端点必须在 UI/其余逻辑继续之前完成?
  • 您可以拆分方法调用并等待:var task = _oDataService.GetElements(_userId, _token); /* Do some work */ await task; 如果您需要的话。
  • 不完全。 APP在一个'LoginViewModel'中启动,登录成功后,继续登录代码并开始'同步'。在同步运行时,用户被重定向到第一个数据页面(例如 HomeView)。登录后的每个 View/ViewModel 都会检查该页面的数据是否已加载,这些“检查”变量在特定的 Get{dataname} 方法中设置。 API 返回结果后,该变量被设置为 true。所以这意味着我必须像你描述的那样将var task设为全局,可以从(其他)ViewModels访问?
  • 但也许我做错了 :) 我对移动开发/API 同步数据很陌生。

标签: c# xamarin.forms


【解决方案1】:

由于这是一个常见问题,我发布我的评论作为答案。问题:一个页面需要 在显示之前获取一些异步数据或执行一些异步检查。但是页面的构造函数不能 异步(有充分的理由,构造函数必须是最小的)。有时会绕过这个问题 使用不好的解决方案(使用 Wait()Result 阻止调用,使用 Task.Run 触发并忘记,...)。

解决方案:ViewModel 的异步创建模式 您可以在 ViewModel 中定义静态 CreateAsync() 方法:

public class ViewModelB
{
    public string DataFromApi { get; }

    // private constructor to prevent direct instantiation
    private ViewModelB(string dataFromApi)
    {
        DataFromApi = dataFromApi;
    }

    public static async Task<ViewModelB> CreateAsync()
    {
        // Load data from HttpClient, ...
        var loadedData = await client.GetStringAsync();
        // Now we have all data to instantiate the view model.
        return new ViewModelB(loadedData);
    }
}

现在您可以向您的页面类添加一个构造函数,该构造函数使用初始化的视图模型来设置 绑定上下文:

public partial class PageB : ContentPage
{
    // private constructor to prevent direct instantiation
    private PageB()
    {
        InitializeComponent();
    }

    // Our page needs an initialized vm.
    public PageB(ViewModelB vm) : this()
    {
        BindingContext = vm;
    }
}

在页面 A 的事件处理程序(登录按钮,...)中,可以是异步的,您可以为 页面 B 并将其传递给您的页面构造函数:

try
{
    var vm = await ViewModelB.CreateAsync();
    await myNavigation.PushAsync(new PageB(vm));
}
catch 
{
    // Give feedback if there was a problem during initialization (server error, authorization problems,  ...)            
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2013-11-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-02
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多