Share on Twitter
Share on Facebook
Share on HackerNews
Share on LinkedIn

Making a Time Zone Picker Control for .NET MAUI

This post is part of the MAUI UI July community series of blog posts and videos, hosted by Matt Goldman. Be sure to check out the other posts in the series!

Hi, my name is Matt Johnson-Pint. I recently joined Sentry as an engineer working on the Sentry .NET SDKs. One of my first big projects was adding support for .NET MAUI, which we’ve now launched in preview. Go ahead, give it a try!

I’m also a time zone nerd. Yep, while others run away screaming from this topic, I dive right in. If you’ve ever asked a technical question on the Internet about time zones, there’s a good chance we’ve met. Ah, yes - time zones. They are absolutely essential to our modern world, but they can be a pain to deal with - especially for software developers! Most of our platforms and languages include features for working with time zones, but sometimes it takes a deeper level of understanding to use those features correctly. When it comes to cross-platform mobile development, such as offered with .NET MAUI, providing a consistent experience can be quite challenging.

So what does Sentry have to do with time zones? Well, not much really, but now that I’m a .NET MAUI developer, I thought it would be good to contribute some open source goodness! For your enjoyment, I’ve made a free MAUI Time Zone Picker Control, which provides a consistent cross-platform user experience for selecting a time zone from a list. You can use it anywhere (and from any time zone) you like! Perhaps you’re making an event scheduling app? I bet you’ll need to pick a time zone for your event.

For good measure, the sample app included with the project also uses the Sentry .NET MAUI SDK. If the sample app crashes on your device, I’ll know about it automatically! Hopefully that means over time I’ll get to improve this control to make it even better.

In this blog post, I’m going to take you on a journey of how I put together the MAUI time zone picker control. My goal is both to teach you something about time zones, and to hopefully provide some insight into how to write custom user interface controls for .NET MAUI in general.

What’s so hard about time zones?

One of the biggest challenges with time zones is simply identifying them. Ask someone what their time zone is, and you may get a variety of different answers, such as “Central Time”, “Central Standard Time”, “Central Daylight Time”, or “Chicago Time”. You might get an abbreviation such as “CST”, “CDT” or “CT”. Sometimes you’ll get a numerical value such as “UTC-6”, “GMT-6, “UTC-06:00”, “UTC-5”, “GMT-5, or “UTC-05:00”. These are all used for the third official time zone of the United States (yes, the third). Some of these names apply all the time, and some apply only at particular parts of the year. That’s just for one time zone on the Earth, and that’s just in English. In general, time zone names and abbreviations are localized. If you speak French, you might call the same time zone “Heure Normale du Centre” and abbreviate it “HNC”.

Time zone abbreviations also are super confusing because the world is a big place, and there are lots of countries that start with the same letters. So if you just represent a time zone as “CST”, without additional context there’s no way to know if that means “Central Standard Time”, “Cuba Standard Time”, or “China Standard Time”.

To solve this problem in computing, we use time zone identifiers. In the past there were multiple sets of time zone identifiers, but they have mostly all coalesced and we now use those from the IANA Time Zone Database in just about every programming language, platform, and operating system. Even Microsoft Windows (which used to only use its own set of identifiers) has started adopting IANA - so you can use them in .NET. The IANA time zone identifier for the zone mentioned above is ”America/Chicago”. It is an exact string that can be used as an ID across platforms, and is never subject to localization.

Here’s where we get to the interesting part. Do we really want to show an IANA time zone ID to our user? What if they only understand Japanese? How will an English language string help them? While you may have seen some applications present a list of these strings for the user to pick from, that’s not the recommended approach. Indeed, the advice from the IANA time zone maintainers themselves is that these should not be shown to the user. Rather, strings displayed to a user should come from other sources such as the Unicode Common Locale Data Repository (CLDR).

So - Let’s make a time zone picker that shows the Unicode strings, each one backed by its corresponding IANA time zone identifier. We’ll also give the location and current UTC offset for each one, to assist the user in making their selection. We want it to be a scrollable list that looks like this:

.NET MAUI Time Zone Picker - Design

Note, this is a very rough first attempt - I’ve got a lot of ideas to make this even better over time!

Creating the .NET MAUI Control Projects

I’ll assume you’ve already set up your dev environment for working with MAUI. If not, check out Microsoft’s getting started docs before continuing.

The first step in our journey is to make a new .NET project. Since this is a reusable component, I’ll create a MAUI class library. I’ll also want a sample MAUI app, and a test project, and link them together.

mkdir MauiTimeZonePicker
cd MauiTimeZonePicker 
dotnet new mauilib -n MauiTimeZonePicker -o src
dotnet new maui -n MauiTimeZonePickerSampleApp -o sample
dotnet new xunit -n MauiTimeZonePicker.Tests -o test
dotnet add sample reference src
dotnet add test reference src

Note, you may want to delete the sln file created in the sample directory and make a new one in the root.

Now I can launch VS Code and start editing. You could also use Visual Studio, VS for Mac, or JetBrains Rider, but you will need the latest preview editions of each to use MAUI. For this blog post I’ll stick to the command line and VS Code.

Creating the Control and its Elements

We’re going to be building a new control, but we don’t want to do it from scratch. Instead, we’ll start with a layout so we can add other items to it. There are many types of layouts available. To fit our design, we’ll use a VerticalStackLayout, which just places each item one on top of the other.

namespace MauiTimeZonePicker;

public class TimeZonePicker : VerticalStackLayout
{
}

Then we’ll add some content to the layout. First a Label for the header, and then a Border for the box below it. Within the border, we’ll place a CollectionView control, which will do most of the heavy lifting to show our time zone list.

using Microsoft.Maui.Controls.Shapes;

namespace MauiTimeZonePicker;

public class TimeZonePicker : VerticalStackLayout
{
    public TimeZonePicker()
    {
        AddContent();
    }

    private void AddContent()
    {
        Add(new Label
        {
            Text = "Pick a time zone...",
            FontSize = 14
        });

        Add(new Border
        {
            StrokeShape = new Rectangle(),
            Padding = 2,
            Content = GetCollectionView()
        });
    }

    private CollectionView GetCollectionView()
    {
        var view = new CollectionView
        {
            //ItemsSource = TODO,
            //ItemTemplate = TODO,
            MaximumHeightRequest = 200,
            SelectionMode = SelectionMode.Single
        };

        ((LinearItemsLayout) view.ItemsLayout).ItemSpacing = 10;

        return view;
    }
}

One major “gotcha” to point out - if you use a Border control, be sure to set a StrokeShape. Otherwise not only will you not get a border, but the inner content will be invisible when rendered on iOS and macCatalyst. That may be a bug in MAUI, but it’s easy to encounter.

Next, we’ll want to create an item template for our control, and pass some items for it to render! You can check out the full template I used in the source code repository here.

Getting the Time Zone Resource Data

So far, making the control seems to be pretty straightforward - but where will the data come from? We could try using the TimeZoneInfo object, but we would find that it doesn’t necessarily have all of the localized strings that we need. As mentioned earlier, we want most of our data to come from Unicode’s CLDR project. Well, it turns out that this data is already available on all of the platforms we are using - often through a platform-specific implementation of Unicode’s ICU library (International Components for Unicode), which is distributed by the operating system.

Since .NET MAUI has excellent support for calling platform-specific code, let’s leverage that.

  • On Android, we have access to Android’s implementation of ICU. Android exposes Java classes, and .NET MAUI makes them available to us via C# in the Android.Icu namespace.

  • On iOS and macCatalyst, we have access to Apple Foundation classes that access data from IANA and ICU under the hood, including NSTimeZone, NSLocale, and NSDateFormatter. These are also all available via C# in the Foundation namespace.

  • On Windows, we have access to Windows supported ICU via C apis only. However, we can still access these from our .NET MAUI code with a bit of P/Invoke. These can get a bit complicated, but the ICU4C API reference comes in handy. Just note that we can only call C APIs - not C++.

If you want to deep dive into all of this, check out internals of the TimeZoneResourceProvider class in the source code repository. Be sure to notice that there are several different platform-specific implementations of the same methods. Thanks, MAUI!

Once we have the data, we can make some methods for accessing it, and call those from our control. The details don’t really matter here, as long as we expose the end result to the ItemSource of the collection view. I’ve designed my TimeZoneResourceProvider to provide a list of TimeZoneResource objects, each of which has the various information needed to display and use a particular time zone.

Responding to Events

It’s not enough that we are displaying a list, we must also make it possible for a user to select an item from it. We may then need to then take action when this selection has been made. Most of this is handled for us by our use of the CollectionView control, but we still need to expose that to the user. I’ll also expose the collection view itself so we can set any additional properties if desired.

public CollectionView CollectionView { get; }

public TimeZoneResource? SelectedItem => CollectionView.SelectedItem as TimeZoneResource;

public event EventHandler<SelectedItemChangedEventArgs>? SelectedItemChanged;

public class SelectedItemChangedEventArgs : EventArgs
{
    public SelectedItemChangedEventArgs(TimeZoneResource? previousSelection, TimeZoneResource? currentSelection)
    {
        PreviousSelection = previousSelection;
        CurrentSelection = currentSelection;
    }

    public TimeZoneResource? PreviousSelection { get; }
    public TimeZoneResource? CurrentSelection { get; }
}

We can delegate the collection view’s SelectionChanged event to our own, when we create the control.

view.SelectionChanged += (_, args) =>
{
    var previousSelection = args.PreviousSelection.FirstOrDefault() as TimeZoneResource;
    var currentSelection = args.CurrentSelection.FirstOrDefault() as TimeZoneResource;
    var eventArgs = new SelectedItemChangedEventArgs(previousSelection, currentSelection);
    SelectedItemChanged?.Invoke(this, eventArgs);
};

Now the user will have a strongly typed SelectionChanged event and SelectedItem property on our TimeZonePicker.

Using our Control in an Application

Now that we’ve built the control, we can use it in application. Ok - you’ve probably already beat me to this step, as it would be difficult to get this far without trying to look at it a few times along the way. But anyway, here’s how you add it:

In your application, make a reference to the class library project containing the custom control and supporting code. We did this already with the dotnet add command earlier, but you could also add a project reference through Visual Studio, or edit the csproj file directly to add a ProjectReference tag.

<ItemGroup>
  <ProjectReference Include="..\src\MauiTimeZonePicker.csproj" />
</ItemGroup>

Your end-users will make a PackageReference tag instead. For example, if you want to try my current implementation:

<ItemGroup>
  <PackageReference Include="mjp.MauiTimeZonePicker" Version="0.1.0-preview" />
</ItemGroup>

Now add a package reference to Sentry.Maui. Wait, just kidding! You don’t need to use Sentry if you don’t want to. But if you do, it’s pretty easy:

<PackageReference Include="Sentry.Maui" Version="3.19.0-preview.2" />

Lastly, add let’s add the control to your app! Assuming you’re in XAML, you’ll need to import a namespace and give it an alias.

xmlns:mjp="clr-namespace:MauiTimeZonePicker;assembly=MauiTimeZonePicker"

Then you can add the control somewhere in your app. I’ve also assigned a name and am using the event we created.

<mjp:TimeZonePicker x:Name="TimeZonePicker" SelectedItemChanged="TimeZoneChanged" />

Of course, there may be many other things you want your app to do - but as far as using the control goes, it’s pretty straightforward.

Yay, it works

Here’s what the final sample application looks like in my Android emulator.

.NET MAUI Time Zone Picker - Android

Take a look at other platform screenshots in the repository readme.

Testing

It would be great if we could add some unit tests to this project. Unfortunately, most of the code that really needs to be tested is platform-specific. To test it, the unit tests would have to be running on a device. Presently this is a challenge for MAUI developers.

Here at Sentry, we also have the same issue for Sentry.Maui, so we’ve started working on a solution for that. Recently, we’ve also learned that there’s a Xunit.Runners.Maui project under development. Hopefully we’ll be able to join forces and come up with a solution everyone can use (including my time zone picker)!

Final Thoughts

.NET MAUI is pretty slick. I’ve thoroughly enjoyed working with it. Xamarin laid the groundwork, and it still provides much of the core underpinnings for how MAUI works, but MAUI is much easier. I especially like that it uses the same modern .NET toolchain that I use for everything else I write these days.

I’ll keep iterating on this MAUI time zone picker project. Who knows, perhaps someone will find it useful and it will become super popular. Come join me on GitHub if you want to participate. I’d be happy for feedback and contributions. I am just glad to have custom control to play with, and I was happy to walk through making it with you.

Don’t forget to try out Sentry for all your .NET error monitoring and performance monitoring needs. Our .NET MAUI SDK is brand new, but we’ve got many other happy .NET customers using our product.

If you liked this article, feel free to reach out to me on Twitter at @mattjohnsonpint. Thanks!

Your code is broken. Let's Fix it.
Get Started

More from the Sentry blog

ChangelogCodecovDashboardsDiscoverDogfooding ChroniclesEcosystemError MonitoringEventsGuest PostsMobileMoonlightingOpen SourcePerformance MonitoringRelease HealthSDK UpdatesSentry
© 2024 • Sentry is a registered Trademark
of Functional Software, Inc.