Dev Stories

#CASBAN6: How to set up a local Microsoft SQL database on macOS

#CASBAN6: How to set up a local Microsoft SQL database on macOS

Microsoft’s SQL Server cannot be installed directly on macOS, like on Windows machines. Luckily, there is a not so complicated solution using a Docker container – provided by Microsoft themselves.

Install Docker

Obviously, the first step is to download Docker and install it on your Mac. Just head over to the Docker website and download the appropriate version. Install the app by opening the disk image and follow the instructions.

Install SQL Server

After installing the Docker desktop client, head over to the docker hub of Microsoft’s SQL Server. You can choose between SQL Server 2017, 2019 and 2022, with the latter one being preview (as of publishing time of this post). To download the image, we need to open a terminal and download it with the pull command:

sudo docker pull mcr.microsoft.com/mssql/server:2019-latest

I am selecting 2019 here as it is closest to what Azure SQL databases uses as of publishing time of this post.

Create a server instance

Microsoft makes it quite easy to create a server instance, we just need to copy the appropriate run command from the docker hub website. I am using SQL Server Express for my testing purposes:

 docker run -e 'ACCEPT_EULA=Y' -e 'SA_PASSWORD=thisShouldB3Stronger!' -e 'MSSQL_PID=Express' -p 1433:1433 --name mssql  -d mcr.microsoft.com/mssql/server:2019-latest

Once you run this command without any error, type in docker ps to verify the image is up and running. If all goes well, you should see something like this:

Create a database

Now that we have a running server instance, we can finally create a database for our purposes. We are using this terminal command to achieve our goal:

docker exec -i mssql /opt/mssql-tools/bin/sqlcmd -S localhost -U SA -P 'thisShouldB3Stronger!' -Q 'CREATE DATABASE localDB'

We are logging into our server with this and send the command to create our database. Alternatively, we could already connect using DBeaver(link below) to create the Database. In both cases, we have our local database up and running by now.

Connect

There are several ways to connect to this database. The one we are going to use with Entity Framework Core is the good old connection string:

//template: Data Source=localhost;Initial Catalog=<database>;User ID=sa;Password=<password>

Data Source=localhost;Initial Catalog=localDB;User ID=sa;Password=thisShouldB3Stronger!

If you want to access your database with a GUI, I recommend using either Visual Studio Code with the Azure and SQL workload installed or the Community Edition of DBeaver.

DBeaver Community screenshot

Visual Studio allows connecting on a database level, while DBeaver can be used to connect at server level as well. Both of them also support access to Azure SQL databases, which will be helpful later on.

Conclusion

Microsoft’s SQL Server is not available for macOS. Nonetheless, we are able to quickly set up a Docker container that runs MS SQL and set up a local database for testing. I wrote this post for completeness of the series.

Useful links

Until the next post – happy coding, everyone!

Posted by msicc in Azure, Database, Dev Stories, 3 comments
#CASBAN6: Creating A Serverless Blog on Azure with .NET 6 (new series)

#CASBAN6: Creating A Serverless Blog on Azure with .NET 6 (new series)

Motivation

I was planning to run my blog without WordPress for quite some time. For one, because WordPress is really blown up as a platform. The second reason is more of a practical nature – this project gives me lots of stuff to improve my programming skills. I already started to move my developer website away from WordPress with ASP.NET CORE and Razor Pages. Eventually I arrived at the point where I needed to implement a blog engine for the news section. So, I have two websites (including this one here) that will take advantage of the outcome of this journey.

High Level Architecture

Now that the ‘why’ is clear, let’s have a look at the ‘how’:

There are several layers in my concept. The data layer consists of a serverless MS SQL instance on Azure, on which I will work with the help of Entity Framework Core and Azure Functions for all the CRUD operations of the blog. I will use the powers of Azure API Management, which will allow me to provide a secure layer for the clients – of course, an ASP.NET CORE Website with RazorPages, flanked by a .NET MAUI admin client (no web administration). Once the former two are done, I will also add a mobile client for this blog. It will be the next major update for my existing blog reader that is already in the app stores.

For comments, I will use Disqus. This way, I have a proven comment system where anyone can use his/her favorite account to participate in discussions. They also have an API, so there is a good chance that I will be able to implement Disqus in the Desktop and Mobile clients.

Last but not least, there are (for now) two open points – performance measuring/logging and notifications. I haven’t decided yet how to implement these – but I guess there will be an Azure based implementation as well (until there are good reasons to use another service).

Open Source

Most of the software I will write and blog about in this series will be available publicly on GitHub. You can find the repository already there, including stuff for the next two upcoming blog posts already in there.

Index

I will update this blog post regularly with a link new entries of the series.

Additional note

Please note that I am working on this in my spare time. This may result in delays between the blog posts and the updates committed into the repository on GitHub. On top, I also have to split my spare time between my other side project (TwistReader) and this one (3 days a week for each). Whenever necessary, either one of the projects can take precedence over the other, so be aware – and please understand.

Until the next post – happy coding, everyone!


Title Image by Roman from Pixabay

Posted by msicc in Android, Azure, Dev Stories, iOS, MAUI, Web, 1 comment
Make the IServiceProvider of your MAUI application accessible with the MVVM CommunityToolkit

Make the IServiceProvider of your MAUI application accessible with the MVVM CommunityToolkit

As you might know, I am in the process of converting all my internal used libraries to be .NET MAUI compatible. This is quite a bigger task than initially thought, although I somehow enjoy the process. One thing I ran pretty fast into is the fact that you can’t access the MAUI app’s IServiceProvider by default.

Possible solutions

As always, there is more than one solution. While @DavidOrtinau shows one approach in the WeatherTwentyOne application that accesses the platform implementation of the Services, I prefer another approach that uses, in fact, Dependency Injection to achieve the same goal.

Implementation

I am subclassing the Microsoft.Maui.Controls.Application to provide my own, overloaded constructor where I inject the IServiceProvider used by the MAUI application. Within the constructor, I am using the MVVM CommunityToolkit’s Ioc.Default.ConfigureServices method to initialize the toolkit’s Ioc handler. Here is the code:

using CommunityToolkit.Mvvm.DependencyInjection;

namespace MauiTestApp
{
	public class MyMauiAppImpl : Microsoft.Maui.Controls.Application
	{
		public MyMauiAppImpl(IServiceProvider services) 
		{
            Ioc.Default.ConfigureServices(services);
        }
	}
}

Usage

Using the class is straight forward. Open your App.xaml file and replace the Application base with your MyMauiAppImpl:

<local:MyMauiAppImpl
             xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:local="clr-namespace:MauiTestApp"
             x:Class="MauiTestApp.App">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/Styles/Colors.xaml" />
                <ResourceDictionary Source="Resources/Styles/Styles.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</local:MyMauiAppImpl>

And, of course, the same goes for the code behind-file App.xaml.cs:

namespace MauiTestApp;

public partial class App : MyMauiAppImpl
{
	public App(IServiceProvider serviceProvider) : base(serviceProvider)
	{
		InitializeComponent();

		MainPage = new AppShell;
	}

}

That’s it, you can now use the MVVM CommunityToolkit’s Ioc.Default implementation to access the registered Services, ViewModels and Views.

Conclusion

In this post, I showed you a simple (and even easily reusable way) of making the IServiceProvider of your .NET MAUI application available. I also linked to an alternative approach, if you do not want to subclass the application object, I recommend that way.

As always, I hope this post is helpful for some of you.

Until the next post, happy coding, everyone!
Posted by msicc in Dev Stories, MAUI, Xamarin, 1 comment
Invoke platform code in a MAUI app using the built-in Dependency Injection

Invoke platform code in a MAUI app using the built-in Dependency Injection

In Xamarin.Forms, my internal libraries for MVVM helped me to keep my applications cleanly structured and abstracted. I recently started the process of porting them over to .NET MAUI. I was quickly reaching the point where I needed to invoke platform specific code, so I read up the documentation.

The suggested way

The documentation suggests creating a partial class with partial method definitions and a corresponding partial classes with the partial method implementations (like described in the docs). I tried to follow the above-mentioned MAUI documentation and copy/pasted the code sample in there and thought everything is going to be fine. Well, it was not. I wasn’t even able to compile the solution with that code on my Mac in the first place.

In search for a possible cause of this, I did not find a solution immediately. In the end, it turned out that I needed to implement the partial class method on all platforms specified in the TargetFrameworks within the .csproj file. It should have been obvious due to the fact that MAUI is a single project with multiple target frameworks, but it wasn’t on that day.

On top of that, Visual Studio did some strange changes to the .csproj file specifying unnecessary None, Compile and Include targets that should not be generated explicitly, which added a lot to my confusion as well. After removing them from the project file and adding an implementation for all platforms, I was able to compile and test the code from the docs.

But I love my interfaces!

Likewise, that’s why I did not stop there. Following the abstraction approach, interfaces allow us to define the common surface of the API without worrying about the implementation details. That’s not the case for the partial classes approach, like the problems I had showed.

Luckily for us, .NET MAUI supports multi targeting. This means an interface can have a platform specific implementation while being defined in the shared part of the application. If you have used the MSBuildExtras package before, you know already how that works. Best part – .NET MAUI already provides the multi targeting configuration out of the box.

Show me some code!

First, let’s define a simple interface for this exercise:

namespace MAUIDITest.InterfaceDemo
{
    public interface IPlatformDiTestService
    {
        string SayYourPlatformName();
    }
}

Now we are going to implement the platform specific implementations. Go to the first platforms folder and add a new class named PlatformDiTestService. Then – and this is really important to make it work – adjust the namespace to be the same as the one of the interface. Last, but not least, implement the interface, for example like this:

namespace MAUIDITest.InterfaceDemo
{
    public class PlatformDiTestService : IPlatformDiTestService
    {
        public string SayYourPlatformName()
        {
            return "I am MacOS!";
        }
    }
}

Repeat this for all platforms, and replace the platform’s name accordingly.

Using Dependency Injection in MAUI

If you have been following along my past blog posts, you know that I recently switched to the CommunityToolkit MVVM (read more here and here). I already heard that MAUI will get the same DI container built-in, so the choice was obvious. Now let’s have a look how easy we can inject our interface into our ViewModel. Head over to your MauiProgram.cs file and update the CreateMauiApp method:

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

		//lowest dependency
		builder.Services.AddSingleton<IPlatformDiTestService, PlatformDiTestService>();
		//relies on IPlatformDiTestService
		builder.Services.AddTransient<MainPageViewModel>();
		//relies on MainPageViewModel
		builder.Services.AddTransient<MainPage>();

		return builder.Build();
	}

First, add the registration of the interface and the implementation. In the sample above, the MainPageViewModel relies on the interface and gets it automatically injected by the DI handler. For testing purposes, I even inject the MainViewModel into the MainPage‘s constructor. This is very likely to change in a real world application.

For completeness, here is the MainPageViewModel class:

using System;
using System.ComponentModel;
using System.Windows.Input;
using MAUIDITest.InterfaceDemo;

namespace MAUIDITest.ViewModel
{
    public class MainPageViewModel : INotifyPropertyChanged
    {
        private readonly IPlatformDiTestService _platformDiTestService;
        private string sayYourPlatformNameValue = "Click the 'Reveal platform' button";
        private Command _sayYourPlatformNameCommand;

        public event PropertyChangedEventHandler PropertyChanged;


        public MainPageViewModel(IPlatformDiTestService platformDiTestService)
        {
            _platformDiTestService = platformDiTestService;
        }

        public void OnPropertyChanged(string propertyName)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }

        public string SayYourPlatformNameValue
        {
            get => sayYourPlatformNameValue;
            set
            {
                sayYourPlatformNameValue = value;
                OnPropertyChanged(nameof(this.SayYourPlatformNameValue));
            }
        }

        public Command SayYourPlatformNameCommand => _sayYourPlatformNameCommand ??=
            new Command(() => { this.SayYourPlatformNameValue = _platformDiTestService.SayYourPlatformName(); });
    }
}

And of course, you want to see the MainPage.xaml.cs file as well:

using MAUIDITest.InterfaceDemo;
using MAUIDITest.ViewModel;

namespace MAUIDITest;

public partial class MainPage : ContentPage
{
    private readonly IPlatformDiTestService _platformDiTestService;
    int count = 0;

	public MainPage(MainPageViewModel mainPageViewModel)
	{
		InitializeComponent();

		this.BindingContext = mainPageViewModel;
    }

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

		if (count == 1)
			CounterBtn.Text = $"Clicked {count} time";
		else
			CounterBtn.Text = $"Clicked {count} times";

		SemanticScreenReader.Announce(CounterBtn.Text);
	}
}

Last, but not least, the updated MainPage.xaml file:

<?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="MAUIDITest.MainPage">
			 
    <ScrollView>
        <VerticalStackLayout 
            Spacing="25" 
            Padding="30,0" 
            VerticalOptions="Center">

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />
                
            <Label 
                Text="Hello, World!"
                SemanticProperties.HeadingLevel="Level1"
                FontSize="32"
                HorizontalOptions="Center" />
            
            <Label 
                Text="Welcome to .NET Multi-platform App UI"
                SemanticProperties.HeadingLevel="Level2"
                SemanticProperties.Description="Welcome to dot net Multi platform App U I"
                FontSize="18"
                HorizontalOptions="Center" />

            <Button 
                x:Name="CounterBtn"
                Text="Click me"
                SemanticProperties.Hint="Counts the number of times you click"
                Clicked="OnCounterClicked"
                HorizontalOptions="Center" />


            <Label Text="Test of built in DI:" FontSize="Large" HorizontalOptions="Center"></Label>
            <Label x:Name="PlatformNameLbl" FontSize="Large" HorizontalOptions="Center" Text="{Binding SayYourPlatformNameValue}" />

            <Button Text="Reveal platform" HorizontalOptions="Center" Command="{Binding SayYourPlatformNameCommand}"/>

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

I did not change the default code that comes with the template. You can easily recreate this by using the default MAUI template of Visual Studio and copy/paste the code snippets above to play around with it.

Conclusion

I only started my journey to update my internal libraries to .NET MAUI. I stumbled pretty fast with that platform invoking code, but luckily, I was able to move along. Platform specific code can be handled pretty much the same as before, which I hope I was able to show you in this post. I’ll write more posts on my updating experiences to MAUI as they happen.

Until the next post, happy coding, everyone!
Posted by msicc in Dev Stories, MAUI, Xamarin, 4 comments
Using Microsoft’s Extensions.DependencyInjection package in (Xamarin.Forms) MVVM applications (Part 2)

Using Microsoft’s Extensions.DependencyInjection package in (Xamarin.Forms) MVVM applications (Part 2)

The Key

Our goal is to add keyed registrations to the IServiceCollection, so we need a common denominator to build upon. As I was able to use a string with the SimpleIoc implementation of MVVMLight for years now, I decided to move on with that and created the following, very complex interface:

public interface IViewModelKey
{
    string Key { get; set; }
}

Every ViewModel that should be registered by Key needs to implement that interface from now on in my MVVM environment.

The Resolver

Back in the MVVMLight times, I was able to query the SimpleIoc registrations with the key I was searching for. In the Microsoft.Extensions.DependencyInjection world, things get a bit more complex. While there are different ways to solve the problem (there are some libraries extending the IServiceProvider with additional methods out there, for example), I decided to use the IServiceProvider itself and go down the resolver interface/implementation road.

Let’s have a look at the interface first:

public interface IViewModelByKeyResolver<T> where T : IViewModelKey
{
    public T GetViewModelByKey(string key);
}

Nothing too special here, just a generic implementation of the resolver interface with the requirement of the IViewModelKey implementation from above. This makes the usage pretty straight forward. The more important part here is the implementation, though. Let’s have a look at mine:

public class ViewModelByKeyResolver<T> : IViewModelByKeyResolver<T> where T : IViewModelKey
{
    private readonly IServiceProvider _serviceProvider;

    public ViewModelByKeyResolver(IServiceProvider serviceProvider)
        => _serviceProvider = serviceProvider;

    public T GetViewModelByKey(string key)
        => _serviceProvider.GetServices<T>().SingleOrDefault(vm => vm.Key == key);
}

The registration of the implementation will automatically inject the IServiceProvider instance at runtime for me here. The GetViewModelByKey method searches all registrations of the given type for the key and returns the desired instance.

Registering the Resolver and keyed ViewModels

The registration of the resolver is done like all the other registrations:

this.ServiceDescriptors.TryAddSingleton<IViewModelByKeyResolver<KeyedViewModel>, ViewModelByKeyResolver<KeyedViewModel>>();

Replace KeyedViewModel with your individual type that implements your key interface. That’s it.

For the registration of the KeyedViewModel instances, there is one thing to pay attention to, though. You cannot use the TryAdd{Lifetime} methods here for registration. Instead, just use the Add{Lifetime} method to register them. Here is a sample:

this.ServiceDescriptors.AddSingleton<KeyedViewModel>(new KeyedViewModel("Key1"));
this.ServiceDescriptors.AddSingleton<KeyedViewModel>(new KeyedViewModel("Key2"));
this.ServiceDescriptors.AddSingleton<KeyedViewModel>(new KeyedViewModel("Key3"));
this.ServiceDescriptors.AddSingleton<KeyedViewModel>(new KeyedViewModel("Key4"));
this.ServiceDescriptors.AddSingleton<KeyedViewModel>(new KeyedViewModel("Key5"));

If you know the keyed ViewModels already at the time of your app startup, you can add them right away and create the IServiceProvider instance as shown in my first post. In most cases, however, you will know the information of the keyed instances only at runtime. Luckily, my Xamarin.Forms implementation already has the solution built in. Here is a short reminder:

public ServiceCollection? ServiceDescriptors { get; private set; }

private IServiceProvider? _services;

public IServiceProvider? Services => _services ??= BuildServiceProvider();

public IServiceProvider? BuildServiceProvider(bool resetExisiting = false)
{
    if (this.ServiceDescriptors == null)
        throw new ArgumentNullException($"Please register your Services and ViewModels first with the {nameof(RegisterServices)} and {nameof(RegisterViewModels)} methods.");

    if (resetExisiting)
        _services = null;

    if (_services == null)
        _services = ServiceDescriptors.BuildServiceProvider();

    return _services;
}

The BuildServiceProvider method has an additional parameter that allows to reset the existing IServiceProvider. This way, I can keep my existing registrations and just add the new keyed ones dynamically. Please note that you may need to reinitialize your already registered and used ViewModels under certain circumstances after performing the reset.

Accessing a keyed ViewModel

Last but not least, I need to show you how to access a ViewModel by its key. Luckily, this is not that hard:

KeyedViewModel vm4 = IocManager.Current.Services.GetService<IViewModelByKeyResolver<KeyedViewModel>>().GetViewModelByKey("Key4");
KeyedViewModel vm2 = IocManager.Current.Services.GetService<IViewModelByKeyResolver<KeyedViewModel>>().GetViewModelByKey("Key2");

Conclusion

By switching to the CommunityToolkit.MVVM package and utilizing Microsoft’s Extension.DependencyInjection package together with it, my MVVM environment is ready for upcoming challenges like .NET MAUI. I will be able to use it on all .NET platforms and just need to adapt my Xamarin.Forms implementation to others (which I have done already for one of our internal tools at work in WPF). Even keyed ViewModel instance can be used similarly as before, as I showed you in this post.

As always, I hope this post will be helpful for some of you.

Until the next post, happy coding!

Posted by msicc in Dev Stories, Xamarin, 1 comment
Book review (and recommendation): Learn T-SQL Querying

Book review (and recommendation): Learn T-SQL Querying

The Fundamentals

In the first section of the book, the authors give us an overview about the anatomy of a query (going very deep) and how SQL Server (also on Azure) processes queries. They also explain how SQL Server optimizes queries and how different versions of SQL Server are processing them differently, which can result in different performances for complex queries.

Dos and Don’ts

The second section of the book is guiding us through how Query Execution plans work and how they can help to write more efficient SQL queries. The section has a bunch of tips that developers can use in their everyday life with databases. The perhaps most important part in this section are the two chapters about T-SQL antipatterns. This is the section where I learned the most throughout the book.

Troubleshooting and tools

The last section of the book shows a lot of troubleshooting techniques and how to use them properly. Microsoft’s SQL Server Management Tools itself comes with a bunch of such tools, and the book helps not only to find them, but also to use them correctly. On top, there are also references to some Open Source tools that can be helpful at times. The book closes with the Query Tuning Assistant, which is the recommended tool to perform SQL server updates.

Conclusion

I got the book because my current job requires me to write efficient T-SQL code for the interfaces I am developing. The book already helped me during the reading time to better understand what I am doing and how I can optimize my own queries. It will be one of my reference books in future when it comes to troubleshooting and query performance with SQL. Long story short, if you are working regularly with Microsoft SQL Server (also on Azure), you should have this book in your (digital) bookshelf.

Book metadata

Posted by msicc in Book Review, Dev Stories, Editorials, 0 comments
Using Microsoft’s Extensions.DependencyInjection package in (Xamarin.Forms) MVVM applications (Part 1) [Updated]

Using Microsoft’s Extensions.DependencyInjection package in (Xamarin.Forms) MVVM applications (Part 1) [Updated]

As some of you might remember, I was always a big fan of the MVVMLight toolkit. As the later one is now deprecated and MAUI around, I took a look at the CommunityToolkit.Mvvm, which is officially the successor to MVVMLight.

As stated in the documentation of the new Toolkit, one could now use the Microsoft’s Extensions.DependencyInjection package for anything related to Inversion of Control (which used to be handled by the SimpleIoc implementation of MVVMLight before). Because this is also the built-in way for .NET 6 and web applications, I decided to adapt it already now for my Xamarin.Forms apps (especially my new one I am currently working on).

[Update] Nuget packages

Please note that while the toolkit’s source is now separated from the Windows CommunityToolkit, the documentation isn’t. This can be confusing (as it was for me). On top of that, there are now two Toolkit MVVM packages:

I thought I got it right when writing this blog post initially. After Brandon Minnick from Microsoft pointed me to the right package, I realized I was not. Up on further research, I found also this discussion in the GitHub repo, stating the one and only will be the CommunityToolkit package. Please use only this one if you are following my tutorials here. I updated all mentions of the Toolkit in this post accordingly.

Default IServiceProvider implementation

The toolkit has a default implementation for the IServiceProvider provided by the Extension.DependencyInjection package. You can read about it here in the documentation and see the source here on GitHub. It focuses heavily on thread safety, its usage is pretty strict, and it does not allow adding ViewModels dynamically. If you do not need stuff like this in your app, you’re probably fine using the Ioc.Default implementation of the toolkit.

Custom IServiceprovider implementation

In TwistReader, the application I am currently working on, I had my requirements easily resolved by the SimpleIoc implementation of the MVVMLight toolkit. With the Extensions.DependencyInjection package, I had to move on with a custom implementation, on which we will have a deeper look in this post. Before you move on reading, make sure you have read the documentation.

IIocManagerBase interface

Of course, I wanted my custom implementation to be reusable. So I extended my existing base interface that my applications need to implement:

public interface IIocManagerBase 
{
    IServiceProvider? Services { get; }
    ServiceCollection? ServiceDescriptors { get; }

    IServiceProvider? BuildServiceProvider(bool resetExisiting = false);

    void Initialize(bool useDefaultNavigationService = true);

    void RegisterServices(bool useDefaultNavigationService);

    void RegisterViewModels();
}

I added the ServiceDescriptors property as well as an IServiceProvider property including a method to (re)build the ServiceProvider if needed. Let’s continue by having a look at the Xamarin.Forms base implementation.

FormsIocManagerBase base class

Building up on the interface before, I created a base implementation for my Xamarin.Forms apps. Let’s go a bit into the details.

In the Initialize method, I am just calling the RegisterServices and the RegisterViewModels methods. One important thing to notice is that I am instantiating the ServiceDescriptors property in the RegisterViewModels method. I also add my default services already to collection there. The RegisterViewModels method remains empty in the base implementation.

public virtual void Initialize(bool useDefaultNavigationService = true)
{
    RegisterServices(useDefaultNavigationService);
    RegisterViewModels();
}

public virtual void RegisterServices(bool useDefaultNavigationService)
{
    this.ServiceDescriptors = new ServiceCollection();

    this.ServiceDescriptors.TryAddSingleton<IDialogService>(DependencyService.Get<IDialogService>());
    this.ServiceDescriptors.TryAddSingleton<IActionSheetService>(new ActionSheetService());

    if (useDefaultNavigationService)
        this.ServiceDescriptors.TryAddSingleton<INavigationService>(new NavigationService());
    else
        System.Diagnostics.Debug.WriteLine("***** DON'T FORGET TO REGISTER YOUR INavigationService INSTANCE(S)!  *****");
}

public virtual void RegisterViewModels()
{
}

Until I switched to CommunityToolkit.Mvvm, this was all I had in there (using SimpleIoc for service registrations). Now that I am using the Extensions.DependencyInjection package, there is some more work to do:

public ServiceCollection? ServiceDescriptors { get; private set; }

private IServiceProvider? _services;

public IServiceProvider? Services => _services ??= BuildServiceProvider();

public IServiceProvider? BuildServiceProvider(bool resetExisiting = false)
{
    if (this.ServiceDescriptors == null)
        throw new ArgumentNullException($"Please register your Services and ViewModels first with the {nameof(RegisterServices)} and {nameof(RegisterViewModels)} methods.");

    if (resetExisiting)
        _services = null;

    if (_services == null)
        _services = ServiceDescriptors.BuildServiceProvider();

    return _services;
}

The code is not that complex, but helps with the IServiceProvider instance handling. The BuildServiceProvider method has a reset flag that allows me to rebuild the provider at runtime. One scenario where we can use this one is for adding ViewModel registrations dynamically during the runtime of our app, but.

IocManager in-app implementation

The next code block shows a typical in-app implementation of my IocManager. You may have noticed I am using the TryAdd{Lifetime} methods already before when adding items to the ServiceCollection. This makes sure that I have always just one registration and does not throw an exception if I try to add it again. If you prefer the exception, just switch to the Add{Lifetime} version.

public class IocManager : FormsIocManagerBase
{
    private static IocManager _instance;

    public static IocManager Current => _instance ??= new IocManager();


    public override void Initialize(bool useDefaultNavigationService = true)
    {
        base.Initialize(useDefaultNavigationService);
    }

    public override void RegisterServices(bool useDefaultNavigationService)
    {
        base.RegisterServices(useDefaultNavigationService);

        this.ServiceDescriptors.TryAddSingleton<ITestService, TestService1>();
        this.ServiceDescriptors.TryAddScoped<ITestService, TestedTestService>();
    }

    public override void RegisterViewModels()
    {
        this.ServiceDescriptors.TryAddSingleton<MainViewModel>();
        this.ServiceDescriptors.TryAddSingleton<SecondaryViewModel>();
    }

    public MainViewModel MainVm => this.Services.GetRequiredService<MainViewModel>();
    public SecondaryViewModel SecondaryVm => this.Services.GetRequiredService<SecondaryViewModel>();
}

For my Xamarin.Forms applications, I always use the IocManager implementation as a singleton. This makes it pretty easy with the different lifetimes on all platforms. As you can see, there is nothing complicated in the registration process, I just add both my services and my ViewModels to the ServiceCollection.

I also have some convenience properties for the most important ViewModels that make Binding easier (as I tend to keep code behind files as clean as possible). If you need a service in another place in your app, and you are not using constructor injection (which gets automatically resolved by the Microsoft.Extensions.DepedencyInjection package), you can get the instance in the same way as I do with the ViewModel instances above.

Conclusion

Creating a custom IServiceProvider implementation is not that hard. The custom implementation allows one to recreate the IServiceProvider (handle with care!) if needed. In the next post, I will show you how to deal with keyed ViewModel instances when using the Extensions.DependencyInjection package.

Have you already used the Microsoft.Extensions.DependencyInjection package with Xamarin.Forms or other platforms (not web)? What are your experiences? If so, leave a comment or chat with me on Twitter!

As always, I hope this post is helpful for some of you.

Until the next post – happy coding, everyone!

Posted by msicc in Dev Stories, Xamarin, 4 comments
Goodbye, 2021!

Goodbye, 2021!

Starting at dormakaba

I started this year with a new job, moving from a shareholder register to a company for access solutions. The first month was driven by a lot of explanations and testing. The main task at my new job is to write interfaces that connect our customers’ systems with our access control software. After legally being forced to work from home, I wrote a few interfaces.

During the year, I was able to convince my team of several optimizations in terms of architecture and took also the lead on them. Establishing a core library and building interface standards that just need configuration are those with the most positive impact. We also started to use Bitbucket (the company’s choice) internally, which is also one of my maintenance tasks now.

Overall, I am quite satisfied with my first year at dormakaba even if it was (and is) challenging due to the pandemic.

Side projects

In terms of my side projects, I had a slow year. I did some minor updates to my apps and eventually published them to the stores.

The main focus was put into TwistReader (a twitter list based newsreader), which isn’t available in stores yet.

TwistReader start page (dev view)

Writing this application produced some libraries (partly public/open source, partly private only). My goal is to get the app into TestFlight in the beginning of 2022 and also to write about the libraries.

Blogging

I did not blog much this year. I covered some interesting (hopefully) topics, though:

As I mentioned above, I plan to write about the libraries and also some other stuff I did in Xamarin.Forms next year.

Private stuff

The non technical part of my year was a turbulent one. As I became non-smoker last December, I restarted the sports ambitions I lost when I was young.

I started with running, did some free runs in the beginning, continuing with training for 5 km in under 30 minutes (which I am no able to achieve constantly).

I also started to train for 10 km in under 60 minutes, but was not able to finish the training course yet. I will restart this course in 2022. If you’re into running/cycling as well, feel free to follow me on Strava.

Just running isn’t enough, however, and so I also started with regular functional strength workouts with Freeletics (follow me here).

In our family, things became different as now both of our kids are no longer at school but started to work in apprenticeship. It is amazing to see them both starting they career, even though they routes are different from what we as parents expected. I wish both of my kids the best and the strength they need to succeed.


I wish all of you a Happy New Year, success, a lot of luck and strength for all the challenges of 2022.

Stay safe and healthy, everyone!

Title Image by Markéta Machová from Pixabay

Posted by msicc in Dev Stories, Editorials, 0 comments
Use the iOS system colors in Xamarin.Forms (Updated)

Use the iOS system colors in Xamarin.Forms (Updated)

Update

After publishing this post, Gerald Versluis from Microsoft responded on Twitter with an interesting information on how to get the system colors into our ResourceDictionary without using the DependencyService:

I had a quick look at the NamedPlatformColor class, but noticed that the implementation in Xamarin.Forms is incomplete. Gerald will try to update them. Once that is done, I will update the library on Github and this post again.

Original version below:


Overview

Let me give you a short overview first. To achieve our goal to use the iOS system colors, we need just a few easy steps:

  1. Xamarin.Forms interface that defines the colors
  2. Xamarin.iOS implementation of that interface
  3. ResourceDictionary to make the colors available in XAML
  4. Merging this dictionary with the application’s resource
  5. Handling of the OnRequestedThemeChanged event

Now that the plan is clear, let’s go into details.

ISystemColors interface

We will use the Xamarin.Forms DependencyService to get the colors from iOS to Xamarin.Forms. Let’s create our common interface:

using Xamarin.Forms;

namespace [YOURNAMESPACEHERE]
{
    public interface ISystemColors
    {
        Color SystemRed { get; }
        Color SystemOrange { get; }
        Color SystemYellow { get; }
        Color SystemGreen { get; }
        Color SystemMint { get; }
        Color SystemTeal { get; }
        Color SystemCyan { get; }
        Color SystemBlue { get; }
        Color SystemIndigo { get; }
        Color SystemPurple { get; }
        Color SystemPink { get; }
        Color SystemBrown { get; }
        Color SystemGray { get; }
        Color SystemGray2 { get; }
        Color SystemGray3 { get; }
        Color SystemGray4 { get; }
        Color SystemGray5 { get; }
        Color SystemGray6 { get; }
        Color SystemLabel { get; }
        Color SecondaryLabel { get; }
        Color TertiaryLabel { get; }
        Color QuaternaryLabel { get; }
        Color Placeholder { get; }
        Color Separator { get; }
        Color OpaqueSeparator { get; }
        Color LinkColor { get; }
        Color FillColor { get; }
        Color SecondaryFillColor { get; }
        Color TertiaryFillColor { get; }
        Color QuaternaryFillColor { get; }
        Color SystemBackgroundColor { get; }
        Color SecondarySystemBackgroundColor { get; }
        Color TertiarySystemBackgroundColor { get; }
        Color SystemGroupedBackgroundColor { get; }
        Color SecondarySystemGroupedBackgroundColor { get; }
        Color TertiarySystemGroupedBackgroundColor { get; }
        Color DarkTextColor { get; }
        Color LightTextColor { get; }
    }
}

As we are not able to change any of the system colors, we are just defining getters in the interface.

The Xamarin.iOS platform implementation

The implementation is straight forward. We are implementing the interface and just get the values for each system color. The list is based on Apple’s documentation for human interface and UI element colors.

using [YOURNAMESPACEHERE];

using UIKit;

using Xamarin.Forms;
using Xamarin.Forms.Platform.iOS;

[assembly: Dependency(typeof(SystemColors))]
namespace [YOURNAMESPACEHERE]
{
    //https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/color/
    //https://developer.apple.com/documentation/uikit/uicolor/ui_element_colors

    public class SystemColors : ISystemColors
    {
        #region System Colors
        public Color SystemRed => UIColor.SystemRedColor.ToColor();
        public Color SystemOrange => UIColor.SystemOrangeColor.ToColor();
        public Color SystemYellow => UIColor.SystemYellowColor.ToColor();
        public Color SystemGreen => UIColor.SystemGreenColor.ToColor();
        public Color SystemMint => UIColor.SystemMintColor.ToColor();
        public Color SystemTeal => UIColor.SystemTealColor.ToColor();
        public Color SystemCyan => UIColor.SystemCyanColor.ToColor();
        public Color SystemBlue => UIColor.SystemBlueColor.ToColor();
        public Color SystemIndigo => UIColor.SystemIndigoColor.ToColor();
        public Color SystemPurple => UIColor.SystemPurpleColor.ToColor();
        public Color SystemPink => UIColor.SystemPinkColor.ToColor();
        public Color SystemBrown => UIColor.SystemBrownColor.ToColor();


        public Color SystemGray => UIColor.SystemGrayColor.ToColor();
        public Color SystemGray2 => UIColor.SystemGray2Color.ToColor();
        public Color SystemGray3 => UIColor.SystemGray3Color.ToColor();
        public Color SystemGray4 => UIColor.SystemGray4Color.ToColor();
        public Color SystemGray5 => UIColor.SystemGray5Color.ToColor();
        public Color SystemGray6 => UIColor.SystemGray6Color.ToColor();
        #endregion

        #region UI Element Colors
        public Color SystemLabel => UIColor.LabelColor.ToColor();
        public Color SecondaryLabel => UIColor.SecondaryLabelColor.ToColor();
        public Color TertiaryLabel => UIColor.TertiaryLabelColor.ToColor();
        public Color QuaternaryLabel => UIColor.QuaternaryLabelColor.ToColor();
        public Color Placeholder => UIColor.PlaceholderTextColor.ToColor();
        public Color Separator => UIColor.SeparatorColor.ToColor();
        public Color OpaqueSeparator => UIColor.SeparatorColor.ToColor();
        public Color LinkColor => UIColor.SeparatorColor.ToColor();

        public Color FillColor => UIColor.SystemFillColor.ToColor();
        public Color SecondaryFillColor => UIColor.SecondarySystemFillColor.ToColor();
        public Color TertiaryFillColor => UIColor.TertiarySystemFillColor.ToColor();
        public Color QuaternaryFillColor => UIColor.QuaternarySystemFillColor.ToColor();

        public Color SystemBackgroundColor => UIColor.SystemBackgroundColor.ToColor();
        public Color SecondarySystemBackgroundColor => UIColor.SecondarySystemBackgroundColor.ToColor();
        public Color TertiarySystemBackgroundColor => UIColor.TertiarySystemBackgroundColor.ToColor();

        public Color SystemGroupedBackgroundColor => UIColor.SystemGroupedBackgroundColor.ToColor();
        public Color SecondarySystemGroupedBackgroundColor => UIColor.SecondarySystemGroupedBackgroundColor.ToColor();
        public Color TertiarySystemGroupedBackgroundColor => UIColor.TertiarySystemGroupedBackgroundColor.ToColor();

        public Color DarkTextColor => UIColor.DarkTextColor.ToColor();
        public Color LightTextColor => UIColor.LightTextColor.ToColor();

        #endregion
    }
}

Do not forget to add the Dependency attribute on top of the implementation, otherwise it won’t work.

The ResourceDictionary

As I prefer defining my UI in XAML in Xamarin.Forms, I naturally want those colors to be available there as well. This can be done by loading the colors into a ResourceDictionary. As you might remember, I prefer codeless ResourceDictionary implementations. This time, however, we need the code-behind file to make the ResourceDictionary work for us.

First, add a new ResourceDictionary:

Add_ResourceDictionary_XAML

Then, in the code-behind file, we are using the DependencyService of Xamarin.Forms to add the colors to the ResourceDictionary:

using Xamarin.Forms;
using Xamarin.Forms.Xaml;

[assembly: XamlCompilation(XamlCompilationOptions.Compile)]
namespace [YOURNAMESPACEHERE]
{
    public partial class SystemColorsIosResourceDictionary
    {
        public SystemColorsIosResourceDictionary()
        {
            InitializeComponent();

            this.Add(nameof(ISystemColors.SystemRed), DependencyService.Get<ISystemColors>().SystemRed);
            this.Add(nameof(ISystemColors.SystemOrange), DependencyService.Get<ISystemColors>().SystemOrange);
            this.Add(nameof(ISystemColors.SystemYellow), DependencyService.Get<ISystemColors>().SystemYellow);
            this.Add(nameof(ISystemColors.SystemGreen), DependencyService.Get<ISystemColors>().SystemGreen);
            this.Add(nameof(ISystemColors.SystemMint), DependencyService.Get<ISystemColors>().SystemMint);
            this.Add(nameof(ISystemColors.SystemTeal), DependencyService.Get<ISystemColors>().SystemTeal);
            this.Add(nameof(ISystemColors.SystemCyan), DependencyService.Get<ISystemColors>().SystemCyan);
            this.Add(nameof(ISystemColors.SystemBlue), DependencyService.Get<ISystemColors>().SystemBlue);
            this.Add(nameof(ISystemColors.SystemIndigo), DependencyService.Get<ISystemColors>().SystemIndigo);
            this.Add(nameof(ISystemColors.SystemPurple), DependencyService.Get<ISystemColors>().SystemPurple);
            this.Add(nameof(ISystemColors.SystemPink), DependencyService.Get<ISystemColors>().SystemPink);
            this.Add(nameof(ISystemColors.SystemBrown), DependencyService.Get<ISystemColors>().SystemBrown);


            this.Add(nameof(ISystemColors.SystemGray), DependencyService.Get<ISystemColors>().SystemGray);
            this.Add(nameof(ISystemColors.SystemGray2), DependencyService.Get<ISystemColors>().SystemGray2);
            this.Add(nameof(ISystemColors.SystemGray3), DependencyService.Get<ISystemColors>().SystemGray3);
            this.Add(nameof(ISystemColors.SystemGray4), DependencyService.Get<ISystemColors>().SystemGray4);
            this.Add(nameof(ISystemColors.SystemGray5), DependencyService.Get<ISystemColors>().SystemGray5);
            this.Add(nameof(ISystemColors.SystemGray6), DependencyService.Get<ISystemColors>().SystemGray6);

            this.Add(nameof(ISystemColors.SystemLabel), DependencyService.Get<ISystemColors>().SystemLabel);
            this.Add(nameof(ISystemColors.SecondaryLabel), DependencyService.Get<ISystemColors>().SecondaryLabel);
            this.Add(nameof(ISystemColors.TertiaryLabel), DependencyService.Get<ISystemColors>().TertiaryLabel);
            this.Add(nameof(ISystemColors.QuaternaryLabel), DependencyService.Get<ISystemColors>().QuaternaryLabel);

            this.Add(nameof(ISystemColors.Placeholder), DependencyService.Get<ISystemColors>().Placeholder);
            this.Add(nameof(ISystemColors.Separator), DependencyService.Get<ISystemColors>().Separator);
            this.Add(nameof(ISystemColors.OpaqueSeparator), DependencyService.Get<ISystemColors>().OpaqueSeparator);
            this.Add(nameof(ISystemColors.LinkColor), DependencyService.Get<ISystemColors>().LinkColor);

            this.Add(nameof(ISystemColors.FillColor), DependencyService.Get<ISystemColors>().FillColor);
            this.Add(nameof(ISystemColors.SecondaryFillColor), DependencyService.Get<ISystemColors>().SecondaryFillColor);
            this.Add(nameof(ISystemColors.TertiaryFillColor), DependencyService.Get<ISystemColors>().TertiaryFillColor);
            this.Add(nameof(ISystemColors.QuaternaryFillColor), DependencyService.Get<ISystemColors>().QuaternaryFillColor);

            this.Add(nameof(ISystemColors.SystemBackgroundColor), DependencyService.Get<ISystemColors>().SystemBackgroundColor);
            this.Add(nameof(ISystemColors.SecondarySystemBackgroundColor), DependencyService.Get<ISystemColors>().SecondarySystemBackgroundColor);
            this.Add(nameof(ISystemColors.TertiarySystemBackgroundColor), DependencyService.Get<ISystemColors>().TertiarySystemBackgroundColor);

            this.Add(nameof(ISystemColors.SystemGroupedBackgroundColor), DependencyService.Get<ISystemColors>().SystemGroupedBackgroundColor);
            this.Add(nameof(ISystemColors.SecondarySystemGroupedBackgroundColor), DependencyService.Get<ISystemColors>().SecondarySystemGroupedBackgroundColor);
            this.Add(nameof(ISystemColors.TertiarySystemGroupedBackgroundColor), DependencyService.Get<ISystemColors>().TertiarySystemGroupedBackgroundColor);

            this.Add(nameof(ISystemColors.DarkTextColor), DependencyService.Get<ISystemColors>().DarkTextColor);
            this.Add(nameof(ISystemColors.LightTextColor), DependencyService.Get<ISystemColors>().LightTextColor);

        }
    }
}

That’s all for the implementation. Now let’s start having a look at how to use the whole code we wrote until now.

Merging the ResourceDictionary

In Xamarin.Forms, we are able to merge ResourceDictionary classes to make them available for the whole app or on view/page level only. I consider our above created dictionary as an app-level dictionary. On top, to make it reusable, I put all these classes in a separate multi-platform library, which you can find here on Github.

Please note that the syntax will be a little different if you implement the ResourceDictionary directly in your app. Using the library approach, you will merge the dictionary in this way in App.xaml:

<?xml version="1.0" encoding="utf-8" ?>
<Application
    x:Class="SystemColorsTest.App"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:systemcolors="clr-namespace:MSiccDev.Libs.iOS.SystemColors;assembly=MSiccDev.Libs.iOS.SystemColors">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <systemcolors:SystemColorsIosResourceDictionary />
                <!--  more dictionaries here  -->
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

Responding to system theme changes

Even if I personally only change the system theme at runtime for testing themes in my apps, your users may do so frequently. Luckily, it is just a matter of handling an event to handle this scenario. In your App.xaml.cs file, register for the RequestedThemeChanged event within the constructor:

        public App()
        {
            InitializeComponent();

            Application.Current.RequestedThemeChanged += OnRequestedThemeChanged;

            this.MainVm = new MainViewModel();
            MainPage mainPage = new MainPage()
            {
                BindingContext = this.MainVm
            };

            MainPage = mainPage;
        }

As the system colors respond to the system theme change, we need to reload them to get these changes.

Within the OnRequestedThemeChanged method, we are first getting the actual merged ResourceDictionary instance. Then, we will remove this instance and register a new instance of the ResourceDictionary. This will lead to a full reload of the system colors from iOS into the app. Here is the code:

private void OnRequestedThemeChanged(object sender, AppThemeChangedEventArgs e)
{
    ResourceDictionary iosResourceDict = App.Current.Resources.MergedDictionaries.SingleOrDefault(dict => dict.GetType() == typeof(SystemColorsIosResourceDictionary));

    if (iosResourceDict != null)
    {
        App.Current.Resources.MergedDictionaries.Remove(iosResourceDict);
        App.Current.Resources.MergedDictionaries.Add(new SystemColorsIosResourceDictionary());
    }
}

That’s it, we are now ready to use the colors in XAML and our app adapts to system theme changes. Here is a sample XAML which I wrote to test the colors:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage
    x:Class="SystemColorsTest.MainPage"
    xmlns="http://xamarin.com/schemas/2014/forms"
    xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
    xmlns:local="clr-namespace:SystemColorsTest"
    x:DataType="local:MainViewModel"
    BackgroundColor="{DynamicResource SystemBackgroundColor}">

    <StackLayout>
        <Frame
            Padding="12,42,24,12"
            BackgroundColor="{DynamicResource SystemGray3}"
            CornerRadius="0">
            <Label
                FontSize="36"
                HorizontalTextAlignment="Center"
                Text="iOS SystemColors in XF"
                TextColor="{AppThemeBinding Dark={DynamicResource LightTextColor},
                                            Light={DynamicResource DarkTextColor}}" />
        </Frame>

        <ScrollView>
            <StackLayout BindableLayout.ItemsSource="{Binding SystemColors}">
                <BindableLayout.ItemTemplate>
                    <DataTemplate>
                        <Frame
                            Margin="6,3"
                            x:DataType="local:SystemColorViewModel"
                            BackgroundColor="{Binding Value}">
                            <Label Text="{Binding Name}" />
                        </Frame>
                    </DataTemplate>
                </BindableLayout.ItemTemplate>
            </StackLayout>
        </ScrollView>
    </StackLayout>
</ContentPage>

Please note that I use DynamicResource instead of StaticResource, even if some colors are static. Using DynamicResource forces the app to reload the colors, and there are some that change (like the SystemGray color palette).

Conclusion

Using the iOS system colors in Xamarin.Forms isn’t that complicated with this implementation. If you have more platforms, you could implement the same technique for the other platforms. As I am focusing on iOS for the moment, I just wrote that part. But who knows, maybe this will be extended in the future.

As always, I hope this post will be helpful for some of you.

Until the next post, happy coding, everyone!

Posted by msicc in Dev Stories, iOS, Xamarin, 5 comments
#XFQaD: Compile XAML without code behind in Xamarin.Forms

#XFQaD: Compile XAML without code behind in Xamarin.Forms

How I discovered this #XFQaD

When I was reorganizing the application resources on my current side project, I decided to create some thematically divided ResourceDictionary files. This has led me to do a quick research on Xamarin.Forms resource dictionaries.

If you’re doing this research, you will stumble upon this post from 2018 (!), where the hint to the magic I’ll show you soon was hidden.

Until now, I only used this with ResourceDictionary files. Maybe it will be helpful also for other XAML resources like controls (I will try that in the future).

How to make XAML only files compile

Add a new XAML file (sample is still ResourceDictionary) to your project:

Add_ResourceDictionary

Delete the code behind file, and add your XAML code. Before hitting the Build button, add this line immediately after the .xml file header:

<?xaml-comp compile="true" ?>

This line is where the magic happens. It tells the compiler to include the XAML file in the Build, pretty much like it does with XAML compile attribute in the code behind file.

Conclusion

The only place where I found this hint was the blog post mentioned above. Neither the docs on XAML Compilation nor the docs on resource dictionaries are mentioning this trick. It somehow went quietly from the nightly builds into production code.

Using this trick, we are able to have a more clean project structure. As always, I hope this post will be helpful for some of you.

Until the next post, happy coding!

Posted by msicc in Dev Stories, 9 comments