Learning .NET MAUI – Part 9

Once again, we’ll pick up where we left off. But today we’re in for some big changes.

Let’s add an IsBusy property to use in the MainViewModel. We’ll use the same trick we did with _resultList:

[ObservableProperty]

public bool _isBusy;

Let’s further assume you want an _isNotBusy. There are two ways to accomplish this. One is with a converter from the CommunityToolkit, specifically the InvertBoolConverter. A second option is just to create another member:

public bool _isNotBusy => !IsBusy;

The problem you have is that when IsBusy is changed, you want _isNotBusy to be updated as well. There’s an attribute for that…

[ObservableProperty]
[AlsoNotifyChangeFor(nameof(_isNotBusy))]
public bool _isBusy;

Very clean and tidy.

Using A Service

For the work we’re going to do now, we need another nuget package: System.Text.Json, Microsoft’s new Json manager. Once that is installed, let’s go back to the Model. If you remember the original value we got back, we actually need two classes: root and resultList. We have one, let’s add the other

public class Result
{
    public string zip { get; set; }
    public string city { get; set; }
    public string state { get; set; }
}

public class Root
{
    public List<Result> results { get; set; }
}

For convenience, I put these in the same file, but frankly I wouldn’t do that in a “real” program.

That done let’s create a service to get the zip codes we want. Create a folder named Service and in that create a class ZipCodeService.

public class ZipCodeService
{
    HttpClient _httpClient;
    public ZipCodeService()
    {
        _httpClient = new HttpClient();
    }

    List<Result> _resultList = new();
    Root _root = new();
    
    public async Task<List<Result>> GetResults()
    {
        if (_resultList?.Count > 0)
            return _resultList;

        var url = "https://www.zipwise.com/webservices/citysearch.php?key=yourKey&format=json&string=Middletown";
        var response = await _httpClient.GetAsync(url);

        if (response.IsSuccessStatusCode)
        {
            _root = await response.Content.ReadFromJsonAsync<Root>();
            _resultList = _root.results;
        }

         return _resultList;
    }
    
}

There’s a lot to see here, let’s break it down. We start by creating an HttpClient instance which we initialize in the constructor. We then need two member variables: _resultList and _root which will hold what we get back from the zipWise service. (notice the new initialization syntax. Nifty).

GetResults is the method we care about, it returns a list of Result objects. to get that, we use the url that ZipWise provides (don’t forget to get a ZipWise key). We then call GetAsync and pass in the url.

If we get back a success code we assign our member variable _root to the result of deserializing the JSON using Microsoft’s new JSON package.

Now, remembering that Root contains a list of Result objects, we can assign to the _resultList we created earlier.

Using The Service

With that Service in hand, we can return to MainViewModel and throw away most of what is there. We will no longer be creating our list by hand. We start by declaring a few member fields including one for the ZipCodeServicee

public partial class MainViewModel : ObservableObject
{

    public ZipCodeService _zipCodeService;

    [ObservableProperty]
    public string _title;
    

    [ObservableProperty]
    [AlsoNotifyChangeFor(nameof(IsNotBusy))]
    public bool _isBusy;

    public bool IsNotBusy => !IsBusy;
    
    //[ObservableProperty]
    //public List<Result> _resultList;

    public ObservableCollection<Result> Results { get; } = new();

The two to pay attention to are _zipCodeService and Results. The former does not need the Observable property because it won’t be used as a bindable property, and the latter doesn’t need it because ObservableCollection takes care of updating the UI when anything is added to or removed from it — that is, just as it was in Xamarin.Forms.

public MainViewModel(ZipCodeService _zipCodeService)
{
    Title = "Zip Finder";
    this._zipCodeService = _zipCodeService;
}

In the constructor we set the Title and we assign the local ZipCodeService to the instance injected into the constructor (more on this in just a moment).

Now comes the heart of the matter: GetZipCodesAsync. We’re going to create a button in the UI that calls this method. Here is what the button looks like:

<Button
    Command="{Binding GetZipCodesCommand}"
    IsEnabled="{Binding IsNotBusy}"
    Text="Get Zipcodes" />

Nothing special here, but note that it does call a command in the ViewModel. You would expect to see a command declaration and instantiation but no more! That is all taken care of for you by the code generation if you add an attribute to the method you want the command to call:

 [ICommand]
 public async Task GetZipCodesAsync()

I found this alone pretty mind-blowing.

One quick thing to point out. In the button we bind to GetZipCodesCommand, but in the ViewModel we declared the method as GetZipCodesAsync. That match up is done by the community toolkit! (Convention over configuration).

ICommand]
public async Task GetZipCodesAsync()
{
    if (IsBusy) return;

    try
    {
        IsBusy = true;
        var results = await _zipCodeService.GetResults();
        
        if (Results.Count != 0)
            Results.Clear();

        foreach (var zipCode in results)  
            Results.Add(zipCode);
    }
    catch (Exception ex)
    {
        Debug.WriteLine(ex.Message);
        await Shell.Current.DisplayAlert("Unable to get results!", ex.Message, "OK");
    }
    finally
    {
        IsBusy = false;
    }
}

We start by calling GetResults on the ZipCodeService we instantiated earlier. If you boil this down to the essentials, we call the service, get back the results and then for each zipcode in those results we add them to the Results ObservableCollection declared above.

Dependency Injection

We use dependency injection in the MainPage to inject the view model to set the Binding Context.

public MainPage(MainViewModel viewModel)
{
    BindingContext =viewModel;
    InitializeComponent();

}

Almost done. We need to make an entry in MauiProgram.cs to make the dependency injection work

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .ConfigureFonts(fonts =>
        {
            fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
            fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
        });

    builder.Services.AddSingleton<ZipCodeService>();
    builder.Services.AddSingleton<MainViewModel>();
    builder.Services.AddSingleton<MainPage>();
    return builder.Build();
}

Notice that we’ve registered MainPage and MainViewModel here as singletons and also added a registration for the ZipCodeService. Now everyone can find one-another. and the DI will work.

When you run this with as popular a city name as Middletown, expect to get a long list in return.

You can find the source here.

About Jesse Liberty

Jesse Liberty has three decades of experience writing and delivering software projects and is the author of 2 dozen books and a couple dozen online courses. His latest book, Building APIs with .NET will be released early in 2025. Liberty is a Senior SW Engineer for CNH and he was a Senior Technical Evangelist for Microsoft, a Distinguished Software Engineer for AT&T, a VP for Information Services for Citibank and a Software Architect for PBS. He is a Microsoft MVP.
This entry was posted in Essentials and tagged , . Bookmark the permalink.