mandag den 6. juni 2022

Adding a project to GitHub

At my current work, we use git for managing our project repositories, and while I know that not everyone uses git, I felt like I should make a small write-up of how to make a project repository and commit your project to it.

Even though we use GitLab at work, the process is largely the same for GitHub, which is what I'll be using for this blog.


Initially, you want to make sure you have a git client installed on your computer. I personally mostly use git-cmd which is included in TortoiseGit (I use TortoiseGit for inspecting logs and resolving more complex merge conflicts etc.). 

I will be adding the .NET MAUI project I have made as a part of my 2 previous blogposts to GitHub, so it is easier for people to check my code out, but also to be able to give some examples in the future, of what a workflow in git could look like.


I will start by creating a new repository on GitHub, which can be done under the "Repositories" tab by clicking the "New" button:


This brings me to the page where I can configure my new repo, which looks like this:


Here I choose to set the Repository name to MAUI-samples, as this repository will contain multiple .NET MAUI sample projects, in addition, I add a .gitignore file with the template "VisualStudio" since a Visual Studio project contains some files that we don't want to include in our git repository, and I also add a README file in which I will document the purpose of this repository.


After creating the new repository, I have to commit the project to the repository, and for that, I need the URL of the git repository, which can be found here:


Since this repository will contain multiple sample solutions, I will clone the remote repository, and then simply copy over my existing solution to the newly created repository.



Now after copying over the solution, I will have to add the untracked files to the git index, commit the changes and push to the remote repository.


A brief explanation of the commands:
  • git status - pretty much self-explanatory, but basically shows you the current branch and whether it is up-to-date with the remote branch, as well as and untracked files or uncommitted changes.
  • git add -A - adds files to the git index, -A is the option for "all files".
  • git commit -m "commit message" - Commits the current changes with the commit message provided (-m is the message option).

All that is left now is to push the changes to the remote repository using the git push command:



After refreshing my repository page on GitHub, you can see that the changes have been committed and the files have been added:


The repository can be found here, and I will be adding more content and code to it as I write more posts.

onsdag den 1. juni 2022

Exploring the capabilities of .NET MAUI part 2: Saving and loading language selection in app preferences, skipping selection page if language was loaded from settings

In my last post I briefly touched on how you can implement localization resources in a .NET MAUI app.

By default, .NET will try to find localization resource that corresponds to that of the system that the app is running on, which is pretty convenient.

For this post, however, I wanted to implement a list of languages so that the user can select a language from a language selection page at first app launch, after which the selected language will be saved to app preferences, which is built directly into MAUI for all platforms that it supports.

So, I will implement a ContentPage which its own ViewModel (which is constructor-injected into the ContentPage using .NETs built-in depdendency-injection), which will serve as the first page that is shown only if the preferences contains no saved language.


First things first, I started with creating a simple Language model, which I placed in a "Models" folder in the solution:


namespace localization_demo.Models;

public class Language
{
    public Language(string identifier, string name)
    {
        Identifier = identifier;
        Name = name;
    }

    public string Identifier { get; set; }
    public string Name { get; set; }

    public override string ToString()
    {
        return Name;
    }
}
After this I began writing the code of the ViewModel, but explaining every line of it in text would make this post a bit longer than I would like, so I've instead placed some comments explaining the basic idea:
namespace localization_demo.ViewModels;

public class ChooseLanguageViewModel
{
    public ChooseLanguageViewModel()
    {
        // instantiate ObservableCollection of Language objects
        Languages = new ObservableCollection<Language>
        {
            new Language("en-US", "English"),
            new Language("da-DK", "Danish"),
        };

        // try to load saved language
        LoadSavedLanguagePreferences();
    }


    // this property will serve as the collection which we will show in our contentpage
    public ObservableCollection<Language> Languages { get; private set; }


    // contains the current selection
    private Language _selectedLanguage;
    public Language SelectedLanguage
    {
        get => _selectedLanguage;
        set
        {
            _selectedLanguage = value;
            SetLanguage();
        }
    }


    private void SetLanguage()
    {
        SaveLanguagePreferences(SelectedLanguage);

        // change locale:
        Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo(SelectedLanguage.Identifier);
        Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo(SelectedLanguage.Identifier);
        CultureInfo.CurrentCulture = new System.Globalization.CultureInfo(SelectedLanguage.Identifier);
        CultureInfo.CurrentUICulture = new System.Globalization.CultureInfo(SelectedLanguage.Identifier);

        // continue
        GoToMainPage();
    }


    private void LoadSavedLanguagePreferences()
    {
        if (!Preferences.ContainsKey("locale"))
        {
            return;
        }

        var savedLocale = Preferences.Get("locale", "");
        if (string.IsNullOrEmpty(savedLocale))
        {
            return;
        }

        var correspondingLanguage = Languages.Where(x => string.Compare(x.Identifier, savedLocale) == 0);
        if (correspondingLanguage.Count() != 1)
        {
            // something went wrong here
            return;
        }

        SelectedLanguage = correspondingLanguage.First();
    }


    private void SaveLanguagePreferences(Language language)
    {
        if (language == null)
        {
            return;
        }

        // check if preferences is already set
        if (string.Compare(Preferences.Get("locale", ""), language.Identifier) == 0)
        {
            return;
        }

        Preferences.Set("locale", language.Identifier);
    }


    // //// does not preserve backstack
    private async void GoToMainPage() => await Shell.Current.GoToAsync($"////{nameof(MainPage)}");
}
And then I implemented the ContentPage like so, notice that it sets BindingContext in the constructor to the injected ViewModel in order to bind to objects in xml etc:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:vm="clr-namespace:localization_demo.ViewModels"
             xmlns:model="clr-namespace:localization_demo.Models"
             x:DataType="vm:ChooseLanguageViewModel"
             x:Class="localization_demo.ChooseLanguagePage"
             xmlns:res="clr-namespace:localization_demo.Resources.Strings">
    <VerticalStackLayout Spacing="25" Padding="30" HorizontalOptions="Center">
        <Label Text="{x:Static res:AppResource.Choose_language}"
               SemanticProperties.HeadingLevel="Level1"
               FontSize="32"
               HorizontalOptions="Center" />
        <CollectionView ItemsSource="{Binding Languages}"
                        SelectionMode="Single"
                        SelectedItem="{Binding SelectedLanguage}" >
            <CollectionView.ItemTemplate>
                <DataTemplate x:DataType="model:Language">
                    <Grid Padding="10">
                        <Grid.RowDefinitions>
                            <RowDefinition Height="Auto" />
                        </Grid.RowDefinitions>
                        <Grid.ColumnDefinitions>
                            <ColumnDefinition Width="Auto" />
                        </Grid.ColumnDefinitions>
                        <Label Grid.Column="1"
                               Text="{Binding Name}"
                               FontSize="24" />
                    </Grid>
                </DataTemplate>
            </CollectionView.ItemTemplate>
        </CollectionView>
    </VerticalStackLayout>
</ContentPage>

using localization_demo.ViewModels;

namespace localization_demo;

public partial class ChooseLanguagePage : ContentPage
{
	private readonly ChooseLanguageViewModel _viewModel;

	public ChooseLanguagePage(ChooseLanguageViewModel viewModel)
	{
		InitializeComponent();
		BindingContext = _viewModel = viewModel;
	}
}
However, in order to use dependency-injection in this way, we need to register the types in the dependency-injection system in the CreateMauiApp method, so it ends up looking like this:
public static class MauiProgram
{
	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<ChooseLanguageViewModel>();
		builder.Services.AddTransient<ChooseLanguagePage>();

		return builder.Build();
	}
}
And then lastly, we need to add the new ChooseLanguagePage to our AppShell as a ShellItem, where we also specify the route to it, like so:
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
    x:Class="localization_demo.AppShell"
    xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:localization_demo"
    xmlns:res="clr-namespace:localization_demo.Resources.Strings"
    Shell.FlyoutBehavior="Disabled">

    <ShellContent
        ContentTemplate="{DataTemplate local:ChooseLanguagePage}"
        Route="ChooseLanguagePage" />
    <ShellContent
        ContentTemplate="{DataTemplate local:MainPage}"
        Route="MainPage" />
</Shell>

Now this in by no means a pretty GUI or anything, but the results should be sufficient in terms of being a PoC:

After selecting "Danish", we are redirected to the MainPage of the template app, with out localized strings:


And then if everything is working correctly, the next time we launch the app we should be redirected directly to the MainPage, like so:

And there you have it, this app's localization functionality will work on any platform that is supported by MAUI.

To make this code a bit easier to play around with, I will add this project to a public GitHub repository  in the future, and maybe write a small post about how to do it as well.

Edit: The project can now be found on my GitHub

fredag den 20. maj 2022

Exploring the capabilities of .NET MAUI part 1: Localization in .NET MAUI

Recently I have been following the development of .NET MAUI, which is going to replace Xamarin.Native/Xamarin.Forms, and thought that I might as well document some of my findings here.

Note: As of writing this, you have to install the Visual Studio Preview in order to develop .NET MAUI apps.


To demonstrate how to localize your MAUI app, I will be using the default .NET MAUI App template, as it already provides a simple app that is a good candidate for illustrating something simple like localization.




After creating a new project from a template, I like to build and run it as is, just to ensure that everything is working ok. The default MAUI template app is just a basic hello world with a button that increases a counter and updates the "Current count..." label text to reflect the counter, it looks like this:


We need to add a few resource files that contain the localized strings, and I chose to create a new folder in the Resources directory of my project, called Strings, so as to contain the localization strings (is handy if you support a lot of languages) to which I add one Resources File per language I want to support.


I want to support English and Danish in my app, so after adding adding AppResource.resx for the default English localization, and AppResource.da.resx for the Danish localization, we should have something similar to this:


Let's start with making the default language resource file - Visual Studio provides us with an easy way to insert strings to this resource file, so now we just want to move all the strings from the default MAUI template app into our default AppResource.resx file, like so:


After moving all the strings, we can now access the strings by referencing the resource file's namespace in xaml and access the strings directly, like so:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="localization_demo.MainPage"
             xmlns:res="clr-namespace:localization_demo.Resources.Strings"> <!-- here we declare the namespace that contains the strings -->

    <ScrollView>
        <VerticalStackLayout Spacing="25" Padding="30">

            <Label 
                Text="{x:Static res:AppResource.Hello_World}"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />

            <Label 
                Text="{x:Static res:AppResource.Welcome}"
                SemanticProperties.HeadingLevel="Level1"
                SemanticProperties.Description="{x:Static res:AppResource.Welcome_Desc}"
                FontSize="18"
                HorizontalOptions="Center" />

            <Label 
                Text=""
                FontSize="18"
                FontAttributes="Bold"
                x:Name="CounterLabel"
                HorizontalOptions="Center" />

            <Button 
                Text="{x:Static res:AppResource.Click_me}"
                FontAttributes="Bold"
                SemanticProperties.Hint="{x:Static res:AppResource.Click_me_Hint}"
                Clicked="OnCounterClicked"
                HorizontalOptions="Center" />

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="{x:Static res:AppResource.dotnet_bot_png_Desc}"
                WidthRequest="250"
                HeightRequest="310"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>
</ContentPage>


The CounterLabel text is empty in the xaml because I set it programmatically in the MainPage constructor:

using localization_demo.Resources.Strings;
namespace localization_demo;

public partial class MainPage : ContentPage
{
	int count = 0;

	public MainPage()
	{
		InitializeComponent();
		UpdateCounterLabelText(); // set CounterLabel text
	}

	private void UpdateCounterLabelText()
    {
		CounterLabel.Text = $"{AppResource.Current_count} {count}";
	}

	private void OnCounterClicked(object sender, EventArgs e)
	{
		count++;
		UpdateCounterLabelText();

		SemanticScreenReader.Announce(CounterLabel.Text);
	}
}


We can add the same strings to the AppResource.da.resx file and translate them like so:


Which enables us to switch the resource using the Thread.CurrentThread.CurrentCulture and Thread.CurrentThread.CurrentUICulture APIs provided by .NET. For illustrative purposes, I modified the AppShell constructor to change locale to Danish:

public AppShell()
{
    // change locale to da-DK:
    Thread.CurrentThread.CurrentCulture = new System.Globalization.CultureInfo("da-DK");
    Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("da-DK");
    InitializeComponent();
}



Now you can create a language selection screen to let the user pick their preferred language, or even let .NET automatically pick the current language of the system the app is running on!

Incidentally, this is what my next post will cover, since I wanted to keep this one short.

Edit: The project can now be found on my GitHub