MAUI

How to lock orientation at runtime on iOS 16 with .NET MAUI and Xamarin.Forms

How to lock orientation at runtime on iOS 16 with .NET MAUI and Xamarin.Forms

The old way

Before iOS 16, it was pretty easy to lock a Page into a certain orientation. It was basically just one line of code (if you don’t count the DependencyService boilerplate code in):

UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)UIInterfaceOrientation.Portrait), new NSString("orientation"));

By calling this method whenever the size of a page was allocated, we were able to lock the orientation at runtime with Xamarin.Forms. With iOS 16, this does no longer work – even on native iOS applications.

The new way

To understand the why of the new way, you have to understand the SceneDelegate architecture Apple introduced with iOS 13. Before continuing, you should read this blog post by Donny Wals, which explains it very detailed: Understanding the iOS 13 Scene Delegate – Donny Wals.

Now that we know that the SceneDelegate is, we can move on with our implementation.

Page implementation

Both Xamarin.Forms and .NET MAUI implement the SceneDelegate architecture. That’s why we can update our code similarly to what native iOS implementations look like:

var rootWindowScene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()?.FirstOrDefault()) as UIWindowScene;

if (rootWindowScene == null)
    return;

rootWindowScene.RequestGeometryUpdate(new UIWindowSceneGeometryPreferencesIOS(UIInterfaceOrientationMask.Portrait),
error =>
{
    Debug.WriteLine("Error while attempting to lock orientation: {Error}", error.LocalizedDescription);
});

On top, we have to tell the underlying ViewControllers to update their orientation as well:

var rootViewController = UIApplication.SharedApplication.KeyWindow?.RootViewController;

if (rootViewController == null)
    return;

rootViewController.SetNeedsUpdateOfSupportedInterfaceOrientations();
rootViewController.NavigationController?.SetNeedsUpdateOfSupportedInterfaceOrientations();

The ViewController can be informed via the SetNeedsUpdateOfSupportedInterfaceOrientations method that it needs to redraw its view. If we put this all together, we can have a reusable implementation for our DeviceOrientationService implementation:

private void SetOrientation(UIInterfaceOrientationMask uiInterfaceOrientationMask)
{
    var rootWindowScene = (UIApplication.SharedApplication.ConnectedScenes.ToArray()?.FirstOrDefault()) as UIWindowScene;
    
    if (rootWindowScene == null)
        return;
    
    var rootViewController = UIApplication.SharedApplication.KeyWindow?.RootViewController;

    if (rootViewController == null)
        return;
    
    rootWindowScene.RequestGeometryUpdate(new UIWindowSceneGeometryPreferencesIOS(uiInterfaceOrientationMask),
    error =>
    {
        Debug.WriteLine("Error while attempting to lock orientation: {Error}", error.LocalizedDescription);
    });
    
    rootViewController.SetNeedsUpdateOfSupportedInterfaceOrientations();
    rootViewController.NavigationController?.SetNeedsUpdateOfSupportedInterfaceOrientations();
}

To keep our existing code for older iOS versions working as well. We now just check if we are on iOS 16 and call our new method, below we still can use our traditional way:

public void LockPortrait()
{
    if (UIDevice.CurrentDevice.CheckSystemVersion(16, 0))
    {
        _applicationDelegate.CurrentLockedOrientation = UIInterfaceOrientationMask.Portrait;
    
        SetOrientation(UIInterfaceOrientationMask.Portrait);
    }
    else
    {
        UIDevice.CurrentDevice.SetValueForKey(new NSNumber((int)UIInterfaceOrientation.Portrait), new NSString("orientation"));
    }
}

This will, however, do nothing without the last, very important step. You may have noticed the CurrentLockedOrientation property on the application delegate member above.

Every time the application has to decide whether to rotate or not, the application:supportedInterfaceOrientationsForWindow: gets called to ask for the supported orientations. Only if the application and the ViewController agree on the supported orientations, the action will be executed.

Extending our AppDelegate

Just as on native iOS, we need to implement the method above in our AppDelegate. Xamarin and .NET MAUI do this via the Export attribute, which tells the compiler to override the eventually existing native implementation.

For my solution, I created a derived version of the FormsApplicationDelegate / MauiUIApplicationDelegate classes, passing the current desired UIInterfaceOrientationMask value to the CurrentLockedOrientation property. Finally, I implement the GetSupportedInterfaceOrientationsForWindow method and just return the value of the CurrentLockedOrientation property:

public abstract class AppDelegateEx : MauiUIApplicationDelegate
{
    public virtual UIInterfaceOrientationMask CurrentLockedOrientation { get; set; }

    //according to the Apple docs, Application and ViewController have to agree on the supported orientation, this forces it
    //https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623107-application?language=objc
    [Foundation.Export("application:supportedInterfaceOrientationsForWindow:")]
    public virtual UIInterfaceOrientationMask GetSupportedInterfaceOrientationsForWindow(UIApplication application, UIWindow forWindow)
        => this.CurrentLockedOrientation;
}

Now we just make the AppDelegate derive from AppDelegateEx (or whatever you call it) to finish the implementation for the orientation lock. Finally, locking the orientation works also on iOS 16.

Samples

I created two samples – one for Xamarin.Forms and one for .NET MAUI. The sample work similar on both platforms, and I wrote the code in a reusable way. You can find the samples in the corresponding GitHub repo.

Conclusion

It took me some time to figure out why the traditional way of locking the orientation doesn’t work any longer. After some research and some trial-and-error coding, I was able to come up with a clean and easy-to use solution, which is also reusable. I also learned some new things, like how MAUI implements Scenes and ViewControllers and got a better understanding of the iOS application structure and lifecycle on newer OS versions.

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

Until the next post, happy coding!


Helpful links:


Title Image created via Bing Create with AI

Posted by msicc in Dev Stories, iOS, MAUI, Xamarin, 2 comments
Deploy MAUI apps with Rider on your iOS device after these Xcode errors

Deploy MAUI apps with Rider on your iOS device after these Xcode errors

The Error

I am working a lot with .NET MAUI lately (both at work and for my private projects) and I use JetBrains Rider as the primary IDE on macOS. If you try to deploy an iOS app, your first attempt will likely fail with a big red error message like in the picture above.

The details tell us that the error is coming from missing Xcode extensions:

Failed to install application on device MSicc‘s iPhone 13 Pro: 
2023-04-07 07:41:25.382 mlaunch [18882:2811826] Requested but did not find extension point with identifier Xcode.IDEDebugger.VariablesViewQuickLookProvider for extension Xcode.IDEDebugger.SpriteKitQuickLookProvider of plug-in com.apple.IDESpriteKitParticleEditor 
2023-04-07 07:41:25.384 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.IDEDebugger.VariablesViewQuickLookProvider for extension Xcode.SpriteKit.GKStateMachineQuickLookProvider of plug-in com.apple.IDESpriteKitParticleEditor 
2023-04-07 07:41:25.476 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.WatchApplication of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.476 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.DataSourceConnection for extension Xcode.DebuggerFoundation.watchOSSimulator.DataSourceConnectionTargetHub of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.476 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.Application of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.476 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.Tool of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.ViewDescriber for extension Xcode.DebuggerFoundation.watchOSSimulator.ViewDescriber of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.IntentsService-AppExtension of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.InfoEditorType for extension Xcode.Xcode3ProjectSupport.InfoEditorType.WatchOS.Bundle of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.WatchKit2-AppExtension of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.InfoEditorSlice for extension Xcode.Xcode3ProjectSupport.InfoEditorSlice.WatchOS.BundleInfo of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.ViewDescriber for extension Xcode.DebuggerFoundation.watchOS.ViewDescriber of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.Framework of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.ExtensionKitAppExtension of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.WatchOS.AppExtension of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.DataSourceConnection for extension Xcode.DebuggerFoundation.watchOS.DataSourceConnectionTargetHub of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.477 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.IDEiPhoneSupport.TargetEditor for extension Xcode.IDEiPhoneSupport.TargetEditor.WatchOS.Application of plug-in com.apple.dt.IDEWatchSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.IDEAppleTVSupportUIFramework of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.IDEAppleTVSupportUI.Application of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.IDEAppleTVSupportUI.AppExtension of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.DataSourceConnection for extension Xcode.DebuggerFoundation.tvOSSimulator.DataSourceConnectionTargetHub of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.IDEAppleTVSupportUI.ExtensionKitAppExtension of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.InfoEditorType for extension Xcode.Xcode3ProjectSupport.InfoEditorType.appletvos.Bundle of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.ViewDescriber for extension Xcode.DebuggerFoundation.ATVSimulator.ViewDescriber of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.DeviceIconProvider for extension Xcode.DebuggerFoundation.DeviceIconProvider.AppleTV of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.ViewDescriber for extension Xcode.DebuggerFoundation.ATV.ViewDescriber of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.DebuggerFoundation.DataSourceConnection for extension Xcode.DebuggerFoundation.tvOS.DataSourceConnectionTargetHub of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.TargetSummaryEditor for extension Xcode.Xcode3ProjectSupport.TargetSummaryEditor.IDEAppleTVSupportUI.XPC of plug-in com.apple.dt.IDEAppleTVSupportUI 
2023-04-07 07:41:25.479 mlaunch[18882:2811826] Requested but did not find extension point with identifier Xcode.Xcode3ProjectSupport.InfoEditorSlice for extension Xcode.Xcode3ProjectSupport.InfoEditorSlice.appletvos.BundleTargetInfo of plug-in com.apple.dt.IDEAppleTVSupportUI 
error MT1006: Could not install the application '/Users/msiccdev/Git/RunnersTools/src/MSiccDev.RunnersTools.Client/bin/Debug/net7.0-ios/iossimulator-x64/MSiccDev.RunnersTools.App.app' on the device 'MSicc‘s iPhone 13 Pro': AMDeviceSecureInstallApplicationBundle returned: 0xe800801c.

What the internet says…

Searching the web for this kind of errors will likely lead you to some solutions/workarounds about CommandLineTools. They all basically recommend the following approach:

  • uninstall existing Xcode CommandLineTools installations
  • install them again (best via Terminal)
  • set the CommandLineTools variable in Xcode and other IDEs to the separately installed tools

Spoiler alert: None of them work in this case.

The solution

The solution for this problem is to modify the .csproj file and the info.plist files of your .NET MAUI app. Visual Studio for Mac does these changes implicitly for you, this is something the Rider team could do as well.

Modifying the .csproj file

First, open the .csproj file in Rider by right-clicking on ‘Edit‘ and selecting ‘Edit ‘YourApp,csproj”. Add this PropertyGroup:

<PropertyGroup Condition="$(TargetFramework.Contains('-ios'))">
 <!--DEBUG ON DEVICE-->
<RuntimeIdentifier>ios-arm64</RuntimeIdentifier>
 <!--DEBUG ON SIMULATOR-->
<!--<RuntimeIdentifier>iossimulator-x64</RuntimeIdentifier>-->
</PropertyGroup>

This will be your manual switch between deploying to the simulator and deploying to your device. Just comment/uncomment the RuntimeIdentifier as you need.

Modifying the info.plist files

The next step is to locate the info.plist files in your iOS (and Mac Catalyst) projects in the Platforms folder. Open both of them with by double click. At the bottom of the editor window, select ‘Text‘:

Jetbrains Rider Plist Editor / Text Switch

Add these lines to before the closing before the last closing dict tag:

<key>CFBundleIdentifier</key>
<string>com.companyname.appname</string>
<key>MinimumOSVersion</key>
<string>15.0</string>

You need to specify the CFBundleIdentifier (just copy the ApplicationId value from your .csproj file) explicitly. Same goes for the MinimumOSVersion – please note that you need to specify the value exactly as in the SupportedOSPlatformVersion property of your .csproj file (for both iOS and Mac Catalyst).

Save all of your modifications and close the solution. I recommend to manually delete the bin and object folders as well. Reopen the solution, let Rider load all the NuGet packages and recreate the bin and object stuff it needs.

When you hit the debug button, your Debug Console will still be full of red messages like those below, but you will be able to deploy (and debug) on your iOS device again.

Jetbrains Rider Debug Console with a lot of errors.

Conclusion

Visual Studio for Mac does all these steps for you. If you try and debug the app with VS4Mac, you will notice the same error messages also there, but they are not blocking you from debugging. If you want to deploy and debug with Rider, you’ll have to perform these extra steps to get it working.

If you want to add yourself to the issues on YouTrack, you can do so on these two posts related to the issue:

https://youtrack.jetbrains.com/issue/RIDER-76794/Cannot-deploy-MAUI-project-to-physical-iOS-device

https://youtrack.jetbrains.com/issue/RIDER-80950/Cant-deploy-a-.NET-MAUI-application-on-my-iPhone-SE

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, macOS, MAUI, 0 comments
How to use the .NET CLI clean-up tools on macOS

How to use the .NET CLI clean-up tools on macOS

Earlier this week, the .NET SDK and Runtimes received some updates. Together with that, also Visual Studio for Mac was updated. Once I got past the installation of all updates, both Visual Studio and Rider were no longer restoring the required NuGet packages for my .NET MAUI project running on .NET 6.

I eventually fixed that issue by cleaning up all the .NET SDKs, Runtimes, workloads and NuGet caches on my MacBook Pro. Read on to learn about the tools I used.

.NET uninstall tool

I have been using the .NET uninstall tool in the past. Unlike on Windows, you have to download the executable from GitHub in its zipped form.

While the releases page shows some terminal commands to unpack and run the tool, they never worked for me as stated there. While I was able to make the new directory with the mkdir command, the unpacking always shows an error. So I opened up Finder and unzipped it manually with the Archive Utility app that ships with macOS.

dotnet-core-uninstall unzipping

After switching to the folder in Terminal, the tool is supposed to show the help. Instead, I got an error showing me that I am not allowed to run this app for security reasons. The OS blocks the execution. If the same happens for you, right click on the extracted executable and select “Open With” followed by “Terminal.app (default)“. This will prompt you with this screen:

app downloaded from the internet message

Once you click on “Open“, a new Terminal window appears. Close this window, it is unusable as we are already in the exited state. Instead, open a new Terminal and change to the installation folder and call the help command:

cd ~/dotnet-core-uninstall
./dotnet-core-uninstall -h
Terminal with dotnet-core-uninstall help

Now that we are able to run the tool, let’s have a look what we have installed by running the dotnet --list command. We need to call the command twice, once for the installed -sdks and once for the installed -runtimes:

dotnet --list-sdks
dotnet --list-runtimes

You may be as surprised (I was, at least) how many versions you are accumulating over time. They never get removed by newer versions (it’s by design, according to Microsoft). To get rid of all versions except the latest, run the following commands with the uninstall tool (again once for — sdk, once for –runtime):

sudo ./dotnet-core-uninstall remove --all-but-latest --sdk
sudo ./dotnet-core-uninstall remove --all-but-latest --runtime

After uninstalling all previous versions, you may have to reinstall the latest .NET 6 SDK again. You could also use the –-all-but [Versions] command to specify the versions explicitly. No matter which way you’re going, if you run the dotnet --list commands again, you should see something similar to this:

dotnet --list command

Download: https://github.com/dotnet/cli-lab/releases

Documentation: https://learn.microsoft.com/en-us/dotnet/core/additional-tools/uninstall-tool?tabs=macos#step-3—uninstall-net-sdks-and-runtimes

dotnet workload command

As I had problems getting the required NuGet packages for my MAUI app, I decided to uninstall all .NET MAUI workloads as well. First, I had a look what is installed with the list command:

dotnet workload list
dotnet workload list result

Once you have that list, you need to call the uninstall command for every single installed workload:

sudo dotnet workload uninstall macos maui-maccatalyst maui-ios maui-android ios maccatalyst maui tvos android

Once they are uninstalled, I cleared the Terminal and installed them all again using the install command:

sudo dotnet workload install macos maui-maccatalyst maui-ios maui-android ios maccatalyst maui tvos android
dotnet workload install result in Terminal

Now we have the latest .NET MAUI workload installed as well as the platform specific workloads as well.

Documentation: https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-workload

dotnet nuget locals

The final clean-up step involves all NuGet caches on your machine. Yes, you read that right, multiple caches. To see them all, run the following command:

dotnet nuget locals all --list

This will get you something like this:

dotnet nuget locals cache results in terminal

Now let’s get rid of all those old NuGet packages:

sudo dotnet nuget locals all --clear

If you’re lucky, you will see this message:

local nuget caches cleared in terminal

My first attempt was not that successful. I needed to open the global packages’ folder in Finder and delete some remaining packages manually. Only after that, I was able to run the clear command with success.

Conclusion

Neither Visual Studio nor the .NET installer perform clean-up tasks on macOS. Until Microsoft changes their mind here, we will have to clean-up old packages manually to keep our system smoothly running. Luckily, there are at least CLI tools around to help us with that job. As always, I hope this blog post will be helpful for some of you.

Until the next post, happy coding, everyone!

Posted by msicc in Dev Stories, macOS, MAUI, Xamarin, 1 comment
#CASBAN6: Function base class (and an update to the DTO models)

#CASBAN6: Function base class (and an update to the DTO models)

After we have been setting up our Azure Function project last time, we are now able to create a base class for our Azure functions. The main goal is to achieve a common configuration for all functions to make our life easier later on.

CRUD defintion

Our API endpoints should provide us a CRUD (Create-Read-Update-Delete) interface. Our implementation will reflect this pattern as follows:

  • A creation endpoint
  • Two read endpoints – one for list results (like a list of posts) and one for receiving details of an entity
  • An update endpoint to change existing entities
  • A delete endpoint

As all of our entities are tied to a single blog’s Id, we will use this base class for all entities besides the Blog entity itself.

The base class

    public abstract class BlogFunctionBase
    {
        internal readonly BlogContext BlogContext;
        internal ILogger? Logger;
        internal JsonSerializerSettings? JsonSerializerSettings;

        protected BlogFunctionBase(BlogContext blogContext)
        {
            BlogContext = blogContext ?? throw new ArgumentNullException(nameof(blogContext));

            CreateNewtonSoftSerializerSettings();
        }

        private void CreateNewtonSoftSerializerSettings()
        {
            JsonSerializerSettings = NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings();

            JsonSerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();

            JsonSerializerSettings.NullValueHandling = NullValueHandling.Ignore;
            JsonSerializerSettings.Formatting = Formatting.Indented;
            JsonSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            JsonSerializerSettings.DateFormatHandling = DateFormatHandling.IsoDateFormat;
            JsonSerializerSettings.DateParseHandling = DateParseHandling.DateTimeOffset;
        }

        public virtual Task<HttpResponseData> Create([HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = null)] HttpRequestData req,
            string blogId) =>
            throw new NotImplementedException();

        public virtual Task<HttpResponseData> GetList([HttpTriggerAttribute(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req, string blogId) =>
            throw new NotImplementedException();

        public virtual Task<HttpResponseData> GetSingle([HttpTriggerAttribute(AuthorizationLevel.Anonymous, "get", Route = null)] HttpRequestData req, string blogId, string id) =>
            throw new NotImplementedException();

        public virtual Task<HttpResponseData> Update([HttpTriggerAttribute(AuthorizationLevel.Anonymous, "put", Route = null)] HttpRequestData req, string blogId, string id) =>
            throw new NotImplementedException();

        public virtual Task<HttpResponseData> Delete([HttpTriggerAttribute(AuthorizationLevel.Anonymous, "delete", Route = null)] HttpRequestData req, string blogId, string id) =>
            throw new NotImplementedException();

    }

Knowing the definition of our API endpoints, there shouldn’t be any surprises with that implementation. As a base class, the definition is of course abstract.

In the constructor, I am setting up how JSON objects will be handled. I am following common practices in formatting. The only thing that is different from using JSON.NET directly is the fact we need to explicitly use the NewtonsoftJsonObjectSerializer.CreateJsonSerializerSettings method to create an instance of the JsonSerializerSettings property.

We also have an internal ILogger? property, which will be used in the derived class to create a typed instance for logging purposes. Last but not least, I am enforcing passing in a BlogContext instance from our Entity Framework implementation.

If you look at the method declaration, you’ll see the authorization level is set to Anonymous. As I am using Azure Active Directory, I am handling the authorization on a separate layer (there will be a post on that topic as well). Besides that, there is nothing special. All methods need a blogId and those endpoints that interact with a resource need a resource id as well.

The blog entity has a similar structure, with some differences in the parameter definitions. To keep things simple, I decided to let it be different from the base class definition above. We will see this in the next post of this series.

Update to the DTO models

While the blog series is ongoing, it still lags a bit behind on what I am currently working on (you can follow the dev branch on GitHub for an up-to-date view). As I am currently working on the administration client for our blog, I started to implement an SDK that can be used by all clients (the blog’s website will also just be a client). See the original post on DTOs here.

To be able to create a generic implementation of the calls to the API endpoints, I needed to create also a base class for the DTO models. It is a very simple class, as you can see:

public abstract class DtoModelBase
{
    public virtual Guid? BlogId { get; set; }

    public virtual Guid? ResourceId { get; set; }
}

This base class allows me to specify a Type that derives from it in the SDK API calls – which was my main goal. Second, I have now a common ResourceId property instead of the Id being named after the class name of the DTO. Both properties are virtual to allow me to specify the Required attributes in the derived classes as needed. You can see these changes on GitHub.

The reason I am writing about the change already today is that it will have impact on how the functions are implemented, as both the API functions and the Client SDK use the same DTO classes.

Conclusion

In this post, we had a look at the base class for our Azure functions we will use as an API and on the updated DTO models. With this prerequisite post in place, we will have a look at the function implementations in the next post.

Until the next post, happy coding, everyone!

Posted by msicc in Azure, Dev Stories, MAUI, 1 comment
#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.

Until the next post – happy coding, everyone!


Title Image by Roman from Pixabay

Posted by msicc in Android, Azure, Dev Stories, iOS, MAUI, Web, 2 comments
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