2

I am having a strange NullReferenceException error when using dependency injection in my .NET MAUI 7 app. The API call to my API/token login URL works perfectly when I instantiate a new HttpClient for the request, but when using a service class, singletonService referencing that class in my builder and a view model to get passed the parameterless constructor requirement in maui, I am getting this error. I have tried other approaches too, such as a service locator approach instead of the view-model approach but it makes no difference. I'm starting to wonder if there is an issue with the .NET MAUI DI system.

Here is all my code for this issue:

MauiProgram.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);
    }
}

LoginPageViewModel.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 (This is the page I recieve the error inside)

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");
    }
}

I have tried many different approaches, even consulted Chat GPT to try get some advice but even Chat GPT is stumped with this one. I have done dependency injection on MVC apps and have never had issues like this.

0

3 Answers 3

3

The DependencyService and builder.Services isn't the same API and not the same dependency injection container. That is also why you get a NullReferenceExceptions.

When trying to resolve from the DependencyService it's not there.

There is two ways to go about this: manually resolving the service, which requires you to make the ServiceProvider available somewhere, that might look like this.

But probably the better way is to use constructor injection. To do that, also register your view model and your page in the dependency injection container.

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

Then let the view model be injected in your view and the service in your view model and all should resolve automagically. So change your page to be:

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

I think the rest of your code is already good.

Sign up to request clarification or add additional context in comments.

1 Comment

I had the same issue. this solved it for me (registering the service, viewmodel and page), thanks Gerald!
0

If you are going to use a MVVM model, which I think you should, you should also make sure to honor that model by having each model take care of its own duties. So the view takes care of the controls and the viewmodel takes care of the logic.

To make Dependency Injection work it is always best to register all services, views and ViewModels right away, so you never have problems with that.

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

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

Then we have the View. Let us separate the view from the logic.

<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();
    }
}

So in our ViewModel we go:

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");
    }
}

Now all look really nice and tidy, doesn't it?

Comments

0

Lets put the builder.Services problem aside for now. (Not saying it is not important)

And lets also ignore the constructor injection of the page and the view model. (How much should you be using Singleton for it will not comment)

Your main focus should be to create reusable HttpClient.

It looks disposable, but actually is not. Just because you program frees resources, it does not mean that the OS will be getting them back.

And even if you make your service, and you keep creating new HTTP clients for each request, you may start getting out of socket exceptions.

And I am yet to see a HTTP factory running fine on all platforms in MAUI. Making it singleton may seem like a good idea at first, but you will realize that when some configurations of the network change, it will plain and simple stop working. And then it is even more headache.

I am saying this, because I see that you decided to make it a service, so I guess you plan to use it a lot.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.