在 .NET MAUI 应用中使用依赖项注入的 NullReferenceException

NullReferenceException using dependency injection in a .NET MAUI app

提问人:JarredThera 提问时间:8/21/2023 最后编辑:StevenJarredThera 更新时间:8/22/2023 访问量:199

问:

在我的 .NET MAUI 7 应用程序中使用依赖项注入时,我遇到了一个奇怪的错误。当我为请求实例化一个新的请求时,对我的 API/令牌登录 URL 的 API 调用可以完美运行,但是当使用服务类、singletonService 在我的构建器中引用该类和视图模型以在 maui 中传递无参数构造函数要求时,我收到此错误。我也尝试过其他方法,例如服务定位器方法而不是视图模型方法,但这没有区别。我开始怀疑 .NET MAUI DI 系统是否存在问题。NullReferenceExceptionHttpClient

这是我针对此问题的所有代码:

毛伊岛程序.cs

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();

        builder.Services.AddSingleton<IHttpService, HttpService>();
    
        builder
            .UseMauiApp<App>()
            .UseMauiCommunityToolkit()
            .ConfigureFonts(fonts =>
            {
                fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
                fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
            });
            
#if DEBUG
        builder.Logging.AddDebug();
#endif

        return builder.Build();
    }
}

HttpService.cs:

public interface IHttpService
{
    Task<HttpResponseMessage> SendAsync(HttpRequestMessage request);
    // Add other HTTP methods as needed
}

public class HttpService : IHttpService
{
    private readonly HttpClient _httpClient;

    public HttpService(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request)
    {
        return await _httpClient.SendAsync(request);
    }
}

登录页面视图模型.cs

public class LoginPageViewModel : BaseViewModel
{
    private readonly IHttpService _httpService;

    public LoginPageViewModel(IHttpService httpService)
    {
        _httpService = httpService;
    }

    // You can add more properties and methods specific to the view model
    public IHttpService HttpService => _httpService;
}

LoginPage.xaml.cs(这是我收到错误的页面)

public partial class LoginPage : ContentPage
{
    private string uText, pText;

    public LoginPage()
    { 
        InitializeComponent();
        var httpService = DependencyService.Get<IHttpService>();
        var viewModel = new LoginPageViewModel(httpService);
        BindingContext = viewModel;
    }

    void OnEntryUsernameTextChanged(object sender, TextChangedEventArgs e)
    {
        uText = userNameEntry.Text;
    }

    void OnEntryUsernameTextCompleted(object sender, EventArgs e)
    {
        uText = ((Entry)sender).Text;
    }

    void OnEntryPasswordTextChanged(object sender, TextChangedEventArgs e)
    {
        pText = PasswordEntry.Text;
    }

    void OnEntryPasswordTextCompleted(object sender, EventArgs e)
    {
        pText = ((Entry)sender).Text;
    }

    protected override bool OnBackButtonPressed()
    {
        Application.Current.Quit();
        return true;
    }

    private async void OnLoginClicked(object sender, EventArgs e)
    {
        var viewModel = (LoginPageViewModel)BindingContext;
        string loginUsername = uText.Trim();
        string loginPassword = pText.Trim();

        //Make API call to login to the server
        //This will be a call to recieve a token which is then attached to the rest
        //of the API calls, as well as to check whether the user exists in the
        //database and has entered correct login details
        
        //HttpClient client = new HttpClient();
        var request = new HttpRequestMessage(
            HttpMethod.Get, "https://localhost:44386/token");
        var collection = new List<KeyValuePair<string, string>>();
        collection.Add(new("grant_type", "password"));
        collection.Add(new("username", loginUsername));
        collection.Add(new("password", loginPassword));
        var content = new FormUrlEncodedContent(collection);
        request.Content = content;
        var response =
--->        await viewModel.HttpService.SendAsync(request); <--- Null reference ex
        await Task.Delay(3000);
        if (response.IsSuccessStatusCode)
        {
            var token = await response.Content.ReadFromJsonAsync<Token>();
    
            await SecureStorage.Default.SetAsync(
                "accessTokenKey", token.access_token);
       
            await Shell.Current.GoToAsync("///home");
        }
        else
        {
            await DisplayAlert("Error", response.StatusCode.ToString(), "Retry");
        }
    }

    private async void OnRegisterPageClicked(object sender, EventArgs e)
    {
        await Shell.Current.GoToAsync("///register");
    }
}

我尝试了许多不同的方法,甚至咨询了 Chat GPT 以尝试获得一些建议,但即使是 Chat GPT 也被这个难住了。我已经对 MVC 应用程序进行了依赖注入,并且从未遇到过这样的问题。

.NET 依赖项注入 httpclient maui nullreferenceexception

评论

0赞 Jianwei Sun - MSFT 8/28/2023
如果其中一个对面临相同问题的其他人有帮助,您可以接受其中一个作为最佳答案。

答:

2赞 Gerald Versluis 8/21/2023 #1

和不是同一个 API,也不是同一个依赖注入容器。这也是为什么你会得到一个.DependencyServicebuilder.ServicesNullReferenceExceptions

当试图从它不存在时解决。DependencyService

有两种方法可以解决这个问题:手动解析服务,这需要你在某个地方提供可用的服务,这可能看起来像这样ServiceProvider

但可能更好的方法是使用构造函数注入。为此,还要在依赖项注入容器中注册视图模型和页面。

builder.Services.AddSingleton<IHttpService, HttpService>();
builder.Services.AddTransient<LoginPageViewModel>();
builder.Services.AddTransient<LoginPage>();

然后,将视图模型注入到视图中,将服务注入到视图模型中,所有这些都应该自动解析。因此,将您的页面更改为:

public LoginPage(LoginPageViewModel viewModel)
{ 
    InitializeComponent();
    
    BindingContext = viewModel;
}

我认为你的其余代码已经很好了。

0赞 Peter Wessberg 8/21/2023 #2

如果你打算使用MVVM模型,我认为你应该这样做,你还应该确保通过让每个模型都承担自己的职责来尊重该模型。因此,视图负责控件,视图模型负责逻辑。

要使依赖注入正常工作,最好立即注册所有服务、视图和 ViewModel,这样您就不会遇到任何问题。

mauiAppBuilder.Services.AddSingleton<IHttpService, HttpService>();

mauiAppBuilder.Services.AddSingleton<LoginPageViewModel>();
mauiAppBuilder.Services.AddTransient<LoginPage>();

然后我们有了视图。让我们将视图与逻辑分开。

<Label Text="Username" />
<Entry
            x:Name="userNameEntry"
            Placeholder="Enter your username"
            Text="{Binding UserName, Mode=TwoWay}"

<Label Text="Password" />
<Entry
            x:Name="PasswordEntry"
            IsPassword="True"
            Placeholder="Enter your password"
            Text="{Binding Password, Mode=TwoWay}" />

<Button Clicked="{Binding LoginCommand}" Text="Login" />
<Button Clicked="{Binding RegisterCommand}" Text="Register" /> 
public partial class LoginPage : ContentPage
{
    public LoginPage(LoginPageViewModel loginPageViewModel)
    {
        InitializeComponent();
    }
}

因此,在我们的 ViewModel 中,我们开始:

public partial class LoginPageViewModel : ObservableObject
{
    private readonly IHttpService _httpService;

    [ObservableProperty]
    private string _userName = string.Empty;

    [ObservableProperty]
    private string _password = string.Empty;

    public LoginPageViewModel(IHttpService httpService)
    {
        _httpService = httpService;
    }

    [RelayCommand]
    private async Task Login()
    {
        var request = new HttpRequestMessage(HttpMethod.Get, "https://localhost:44386/token");
        var collection = new List<KeyValuePair<string, string>>();
        collection.Add(new("grant_type", "password"));
        collection.Add(new("username", UserName));
        collection.Add(new("password", Password));
        var content = new FormUrlEncodedContent(collection);
        request.Content = content;
        var response = await _httpService.SendAsync(request);
        await Task.Delay(3000);
        if (response.IsSuccessStatusCode)
        {
            var token = await response.Content.ReadFromJsonAsync<Token>();

            await SecureStorage.Default.SetAsync("accessTokenKey", token.access_token);

            await Shell.Current.GoToAsync("///home");
        }
        else
        {
            await Application.Current.MainPage.DisplayAlert("Error", response.StatusCode.ToString(), "Retry");
        }
        
    }

    [RelayCommand]
    private async Task Register()
    {
        await Shell.Current.GoToAsync("///register");
    }
}

现在看起来都很好,很整洁,不是吗?

0赞 H.A.H. 8/22/2023 #3

让我们暂时把问题放在一边。 (不是说不重要)builder.Services

我们还要忽略页面和视图模型的构造函数注入。 (你应该为它使用多少 Singleton 不会发表评论)

您的主要关注点应该是创建可重用的 HttpClient。

它看起来是一次性的,但实际上不是。仅仅因为您的编程释放了资源,并不意味着操作系统会恢复它们。

即使你做了你的服务,并且你不断为每个请求创建新的HTTP客户端,你也可能会开始出现套接字异常。

而且我还没有看到 HTTP 工厂在 MAUI 的所有平台上都能正常运行。 乍一看,单例似乎是个好主意,但您会意识到,当网络的某些配置发生变化时,它将简单明了地停止工作。然后就更令人头疼了。

我之所以这么说,是因为我看到你决定把它变成一项服务,所以我想你打算经常使用它。