Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
lawrence_shaun_introducing_net_maui_build_and_deploy_crosspl.pdf
Скачиваний:
50
Добавлен:
26.06.2023
Размер:
5.15 Mб
Скачать

Chapter 10 Remote Data

Now that you have created the model classes that can be mapped to the JSON returned from the Open Weather API, you can proceed to calling the API in order to retrieve that JSON.

Connecting to the Open Weather API

Before you start to build the implementation for accessing the API, you are going to create an interface to define what it should do. This has the added benefit that when you wish to unit test any class that depends on the IWeatherForecastService, you can supply a mock implementation rather than requiring that the unit tests will access the real API. I will cover why that is a bad idea in Chapter 14, but the simple answer here is that you have a limited number of calls you are allowed to make for free and you don’t want unit tests eating that allowance up.

namespace WidgetBoard.Communications;

public interface IWeatherForecastService

{

Task<Forecast> GetForecast(double latitude, double longitude);

}

A common naming approach to classes that interact with APIs is to add the suffix Service to show that it provides a service to the user.

Therefore let’s create your service by adding a new class file and calling it

WeatherForecastService.cs. Add the following contents:

using System.Text.Json;

namespace WidgetBoard.Communications;

public class WeatherForecastService : IWeatherForecastService

{

private readonly HttpClient httpClient;

308

Chapter 10 Remote Data

private const string ApiKey = "ENTER YOUR KEY"; private const string ServerUrl = "https://api.open weathermap.org/data/2.5/onecall?";

public WeatherForecastService(HttpClient httpClient)

{

this.httpClient = httpClient;

}

public async Task<Forecast> GetForecast(double latitude, double longitude)

{

var response = await httpClient

.GetAsync($"{ServerUrl}lat={latitude}&lon={longitude} &units=metric&exclude=minutely,hourly,daily,alerts& appid={ApiKey}")

.ConfigureAwait(false);

response.EnsureSuccessStatusCode();

var stringContent = await response.Content

.ReadAsStringAsync()

.ConfigureAwait(false);

var options = new JsonSerializerOptions

{

PropertyNameCaseInsensitive = true

};

return JsonSerializer.Deserialize<Forecast>(string Content, options);

}

}

309

Chapter 10 Remote Data

You added a fair amount into this class file so let’s walk through it step by step and cover what it does.

First is the HttpClient backing field, which is set within the constructor and supplied by the dependency injection layer. You also have constants representing the URL of the API and also your API key that you generated in the earlier sections.

Next is the main piece of functionality in the GetForecast method. The first line in this method handles connecting to the Open Weather API and passing your latitude, longitude, and API key values. You also make sure to set ConfigureAwait(false) because you do not need to be called back on the initial calling thread. This helps to boost performance a little as it avoids having to wait until the calling thread becomes free.

var response = await httpClient

.GetAsync($"{ServerUrl}lat={latitude}&lon={longitude} &units=metric&exclude=minutely,hourly,daily,alerts&appid= {ApiKey}")

.ConfigureAwait(false);

Then you make sure that the request was handled successfully by calling

response.EnsureSuccessStatusCode();

Note that the above will throw an exception if the status code received was not a 200 (success ok).

Then you extract the string content from the response.

var stringContent = await response.Content

.ReadAsStringAsync()

.ConfigureAwait(false);

310

Chapter 10 Remote Data

Finally, you make use of the System.Text.Json NuGet package you installed earlier in order to deserialize the string content into the model classes that you created.

var options = new JsonSerializerOptions

{

PropertyNameCaseInsensitive = true

};

return JsonSerializer.Deserialize<Forecast>(stringContent, options);

I mentioned earlier that you had to explicitly opt-in to matching your property names to the JSON elements case-insensitively.

You can see from the above code that you can do this through the use of the JsonSerializerOptions class and specifically the

PropertyNameCaseInsensitive property.

Now that you have created the service, you should add your weather widget and make use of the service.

Creating theWeatherWidgetView

In order to create your widget, you need to add a new view. Add a new .NET MAUI ContentView (XAML) into your Views folder and call it WeatherWidgetView. This results in two files being created:

WeatherWidgetView.xaml and WeatherWidgetView.xaml.cs. You need to update both files.

WeatherWidgetView.xaml

<?xml version="1.0" encoding="utf-8" ?> <ContentView

xmlns="http://schemas.microsoft.com/dotnet/2021/maui"

xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:viewmodels="clr-namespace:WidgetBoard.ViewModels"

311

Chapter 10 Remote Data

x:Class="WidgetBoard.Views.WeatherWidgetView"

x:DataType="viewmodels:WeatherWidgetViewModel">

<VerticalStackLayout>

<Label

Text="Today"

FontSize="20"

VerticalOptions="Center"

HorizontalOptions="Start" TextTransform="Uppercase" />

<Label

VerticalOptions="Center"

HorizontalOptions="Center">

<Label.FormattedText>

<FormattedString>

<Span

Text="{Binding Temperature, StringFormat='{0:F1}'}" FontSize="60"/>

<Span

Text="°C" /> </FormattedString>

</Label.FormattedText>

</Label>

<Label

Text="{Binding Weather}" FontSize="20" VerticalOptions="Center"

312

Chapter 10 Remote Data

HorizontalOptions="Center" />

<Image

Source="{Binding IconUrl}" WidthRequest="100" HeightRequest="100"/>

</VerticalStackLayout>

</ContentView>

Some of the above XAML should feel familiar based on the previous code you have written. Some bits are new, so let’s cover them.

Label.FormattedText enables you to define text of varying formats inside a single Label control. This can be helpful especially when parts of the text changes dynamically in length and therefore result in the contents moving around. In your example, you are adding a Span with a text binding to your Temperature property in the view model and a second Span with the degrees Celsius symbol.

The second new concept is the use of the Image control. The binding on the Source property looks relatively straightforward; however, it is worth noting that .NET MAUI works some magic for you here. You are binding a string to the property. Under the hood, .NET MAUI converts the string into something that can resemble an image source. In fact, the underlying type is called ImageSource. Further to this, it will inspect your string and if it contains a valid URL (e.g., starts with https://), then it will aim to load it as a remote image rather than looking in the applications set of compiled resources. .NET MAUI will also potentially handle caching of images for you to help reduce the amount of requests sent in order to load images from a remote source. In order to make use of this functionality,

you need to provide a UriImageSource property on your view model rather than the string property.

313

Chapter 10 Remote Data

The process of converting from one type to another is referred to as TypeConverters and can be fairly common in .NET MAUI. I won’t go into detail on how they work, so please go to the Microsoft documentation site at https://learn.microsoft.com/dotnet/api/system.componentmodel. typeconverter.

WeatherWidgetView.xaml.cs

You also need to make the following adjustments to the WeatherWidgetView.xaml.cs file. This part is required because you haven’t created a common base class for the widget views. At times there can be good reason to create them; however, because you want to keep the visual tree as simple as possible, there isn’t a common visual base class to use.

using WidgetBoard.ViewModels;

namespace WidgetBoard.Views;

public partial class WeatherWidgetView : ContentView, IWidgetView

{

public WeatherWidgetView()

{

InitializeComponent();

}

public IWidgetViewModel WidgetViewModel

{

get => BindingContext as IWidgetViewModel; set => BindingContext = value;

}

}

Now that you have created your widget view, you should create the view model that will be paired with it.

314

Chapter 10 Remote Data

Creating theWeatherWidgetViewModel

The view model that you need to create in order to represent the weather-­ related data that can be bound to the UI requires some work that you

are familiar with and some that you are not as familiar with. Let’s proceed to adding the familiar bits and then walk through the newer concepts. First, add a new class file in the ViewModels folder and call it WeatherWidgetViewModel.cs. The initial contents should be modified to look as follows:

using WidgetBoard.Communications;

namespace WidgetBoard.ViewModels;

public class WeatherWidgetViewModel : BaseViewModel, IWidgetViewModel

{

public const string DisplayName = "Weather";

public int Position { get; set; }

public string Type => DisplayName;

}

The above should look familiar as it is very similar to the ClockWidgetViewModel you created earlier on in the book. Now you need to add in the weather-specific bits.

First, add a dependency on the IWeatherForecastService you created a short while ago.

private readonly IWeatherForecastService weatherForecastService;

public WeatherWidgetViewModel(IWeatherForecastService weatherForecastService)

315

Chapter 10 Remote Data

{

this.weatherForecastService = weatherForecastService;

Task.Run(async () => await LoadWeatherForecast());

}

private async Task LoadWeatherForecast()

{

var forecast = await weatherForecastService. GetForecast(20.798363, -156.331924);

Temperature = forecast.Current.Temperature; Weather = forecast.Current.Weather.First().Main; IconUrl = forecast.Current.Weather.First().IconUrl;

}

Inside of your constructor you keep a copy of the service and you also start a background task to fetch the forecast information. Quite often you wouldn’t start something like this from within a constructor; however, given that you know your view model will only be created when it is being added to the UI, this is perfectly acceptable.

Finally, you need to add the properties that your view wants to bind to.

private string iconUrl; private double temperature; private string weather;

public string IconUrl

{

get => iconUrl;

set => SetProperty(ref iconUrl, value);

}

public double Temperature

316