Last time I covered the code for all the view-models. This time I’ll cover the last few classes, from the root of the project, and wrap everything up with this application.
Here is the XAML for the App.xaml file:
<?xml version="1.0" encoding="UTF-8" ?> <Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:CG.Tools.QuickCrypto" x:Class="CG.Tools.QuickCrypto.App"> <Application.Resources> <ResourceDictionary> <Color x:Key="TextColor">Black</Color> <Color x:Key="WindowColor">White</Color> <Style TargetType="ContentPage" ApplyToDerivedTypes="True"> <Setter Property="Padding" Value="{OnPlatform iOS='30,60,30,30', Default='30'}" /> </Style> <Style TargetType="StackLayout"> <Setter Property="Spacing" Value="10" /> </Style> <Style TargetType="HorizontalStackLayout"> <Setter Property="Spacing" Value="10" /> </Style> <Style TargetType="VerticalStackLayout"> <Setter Property="Spacing" Value="10" /> </Style> <Style TargetType="Label"> <Setter Property="TextColor" Value="{DynamicResource TextColor}" /> <Setter Property="FontFamily" Value="OpenSansRegular" /> </Style> <Style x:Key="Action" TargetType="Button"> <Setter Property="TextColor" Value="{DynamicResource TextColor}" /> <Setter Property="FontFamily" Value="OpenSansRegular" /> <Setter Property="Padding" Value="14,10" /> </Style> <Style x:Key="PrimaryAction" TargetType="Button" BasedOn="{StaticResource Action}"> <Setter Property="TextColor" Value="{DynamicResource TextColor}" /> <Setter Property="BackgroundColor" Value="{DynamicResource WindowColor}" /> </Style> <Style TargetType="DatePicker"> <Setter Property="TextColor" Value="{DynamicResource TextColor}" /> </Style> <Style TargetType="TimePicker"> <Setter Property="TextColor" Value="{DynamicResource TextColor}" /> </Style> </ResourceDictionary> </Application.Resources> </Application>
Here’s the code for the App.xaml.cs file:
namespace CG.Tools.QuickCrypto; /// <summary> /// This class is the code-behind for the main app. /// </summary> public partial class App : Application { /// <summary> /// This constructor creates a new instance of the <see cref="App"/> /// class. /// </summary> /// <param name="shell">The shell to use with the app.</param> public App(AppShell shell) { MainPage = shell; InitializeComponent(); // This hack courtesy of: https://github.com/dotnet/maui/discussions/2370 // (It makes the main window a certain size when it first appears). // I just fixed a couple of things with it, the rest is as I found it. Microsoft.Maui.Handlers.WindowHandler.Mapper.AppendToMapping(nameof(IWindow), (handler, view) => { #if WINDOWS var nativeWindow = handler.PlatformView; nativeWindow.Activate(); var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(nativeWindow); var windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd); var appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId); var width = 640; var height = 880; appWindow.MoveAndResize(new Windows.Graphics.RectInt32((1920 / 2) - width / 2, (1080 / 2) - height / 2, width, height)); #endif }); } }
This class contains a hack to resize the window, when running on Windows O/S. That’s because, there’s apparently no way, in .NET MAUI, to specify a size for the main window – in either the XAML, or the code-behind. Personally, I am flabbergasted that Microsoft hasn’t corrected that little problem, but, here we are, in Preview14, still having to resort to hacking the main window size.
Shakes my head.
Here is the XAML for the AppShell.xaml file:
<?xml version="1.0" encoding="UTF-8" ?> <Shell x:Class="CG.Tools.QuickCrypto.AppShell" xmlns="http://schemas.microsoft.com/dotnet/2021/maui" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:local="clr-namespace:CG.Tools.QuickCrypto" xmlns:views="clr-namespace:CG.Tools.QuickCrypto.Views"> <Shell.FlyoutHeader> <Grid BackgroundColor="{DynamicResource WindowBackgroundColor}" HeightRequest="{OnPlatform iOS=62, Default=42}" Padding="{OnPlatform iOS='0, 5, 0, 25', Default='0, 5, 0, 5'}"> </Grid> </Shell.FlyoutHeader> <Shell.FlyoutBackdrop> <LinearGradientBrush StartPoint="0,0" EndPoint="1,1"> <GradientStop Color="White" Offset="0.4" /> <GradientStop Color="SteelBlue" Offset="0.8" /> </LinearGradientBrush> </Shell.FlyoutBackdrop> <Shell.ItemTemplate> <DataTemplate> <Grid ColumnDefinitions="0.25*,0.75*" Padding="0, 10"> <Image Source="{Binding FlyoutIcon}" VerticalOptions="Center" HorizontalOptions="Center" HeightRequest="25"/> <Label Grid.Column="1" Text="{Binding Title}" FontSize="Medium" FontAttributes="Bold" VerticalOptions="Center"/> </Grid> </DataTemplate> </Shell.ItemTemplate> <FlyoutItem Title="Data Protection" Icon="dp.png"> <ShellContent ContentTemplate="{DataTemplate views:DataProtectionPage}" Route="dataprotection" /> </FlyoutItem> <FlyoutItem Title="AES" Icon="aes.png"> <ShellContent ContentTemplate="{DataTemplate views:AesPage}" Route="aes" /> </FlyoutItem> <FlyoutItem Title="Settings" Icon="settings.png"> <ShellContent ContentTemplate="{DataTemplate views:SettingsPage}" Route="settings" /> </FlyoutItem> <FlyoutItem Title="About" Icon="about.png"> <ShellContent ContentTemplate="{DataTemplate views:AboutPage}" Route="about" /> </FlyoutItem> <TabBar> <Tab Title="Data Protection" Icon="dp.png"> <ShellContent ContentTemplate="{DataTemplate views:DataProtectionPage}" /> </Tab> <Tab Title="AES" Icon="aes.png"> <ShellContent ContentTemplate="{DataTemplate views:AesPage}" /> </Tab> <Tab Title="Settings" Icon="settings.png"> <ShellContent ContentTemplate="{DataTemplate views:SettingsPage}" /> </Tab> </TabBar> </Shell>
Here is the code for the AppShell.xaml.cs file:
namespace CG.Tools.QuickCrypto; /// <summary> /// This class is the code-behind for the main application shell. /// </summary> public partial class AppShell : Shell { /// <summary> /// This constructor creates a new instance of the <see cref="AppShell"/> /// class. /// </summary> public AppShell( AppShellViewModel viewModel ) { // Save the binding context. BindingContext = viewModel; // Wire-up a handler for errors. viewModel.ErrorRaised += async (s, e) => { // How should we format the error? if (null == e.Exception) { await DisplayAlert( "QuickCrypto", e.Message, "OK" ); } else { await DisplayAlert( "QuickCrypto", $"{e.Message}{Environment.NewLine}{Environment.NewLine}" + $"{e.Exception.GetBaseException().Message}", "OK" ); } }; // Make the designer happy. InitializeComponent(); } }
This class wires up handlers for the ErrorRaised
and WarningRaised
events, on the view-model. That way, when the view-model has a problem, we can notify the user, through the UI, without breaking the encapsulation of the view-model, or view, in the process.
Here is the code for the MauiProgram.cs file:
namespace CG.Tools.QuickCrypto; /// <summary> /// This class represents the MAUI program. /// </summary> public static class MauiProgram { /// <summary> /// This method is the main entry point for the program. /// </summary> /// <returns>The <see cref="MauiApp"/> instance.</returns> public static MauiApp CreateMauiApp() { // Create the builder. var builder = MauiApp.CreateBuilder(); // Configure the builder. builder.UseMauiApp<App>() .ConfigureFonts(fonts => { fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular"); }); // Setup the application settings. builder.Services.AddCustomOptions( out AppSettings appSettings ); // Register the application shell. builder.Services.AddSingleton<AppShell>(serviceProvider => { var configuration = serviceProvider.GetRequiredService<IConfiguration>(); var logger = serviceProvider.GetRequiredService<ILogger<AppShellViewModel>>(); var viewModel = new AppShellViewModel( new OptionsWrapper<AppSettings>(appSettings), logger ); var appShell = new AppShell(viewModel); return appShell; }); // Register the views. builder.Services.AddTransient<DataProtectionPage>(); builder.Services.AddTransient<AesPage>(); builder.Services.AddTransient<SettingsPage>(); builder.Services.AddTransient<AboutPage>(); // Register the view-models. builder.Services.AddTransient<DataProtectionPageViewModel>(); builder.Services.AddTransient<AesPageViewModel>(); builder.Services.AddTransient<SettingsPageViewModel>(); builder.Services.AddTransient<AboutPageViewModel>(); // Build the app and return it. return builder.Build(); } }
This class is what configures the application, and it’s DI container. We start by constructing a MauiAppBuilder
instance, which we then use to configure the fonts, for the app. Next, we call the AddCustomOptions
extension method, which we covered in an earlier article.
Recall, that AddCustomOptions
is responsible for reading our encrypted appSettings.json file, from the disk, and making those settings available through the DI container.
Next, we register the AppShell
with the DI container. We do that so we can inject the AppShell
instance, into the App
class’s constructor.
Finally, we register all the views and view-models, with the DI container. This way, we can inject instances in constructors, instead of manually ‘newing’ up instances in the code.
And that’s about it for the code!
CG.Tools.QuickCrypt is a tool that I’ve written, and re-written, in a variety of languages and frameworks, over the years. This flavor isn’t much different that my other versions, except this one runs in the new .NET MAUI framework. The tool itself is handy and something I use almost every week. This version of the tool was a learning opportunity for me, to get more comfortable with the new .NET MAUI framework. I hope you enjoyed the walkthrough. I hope you enjoy the tool.
The code for my project is available HERE.
Before I stop I’ll point out that there are some unfinished bits, in this version of CG.Tools.QuickCrypto.
- The UI fields that should have their contents protected, aren’t – at least not yet. That’s because, the
IsPassword
property, on the UI controls, in .NET MAUI, breaks the databinding, leaving the control empty, at runtime. - Not only that, the controls tend to write their contents to the corresponding view-model property backwards – at least when the
IsPassword
property is set to true. Hopefully, those two little problems will be fixed by Microsoft. Until then, the controls are not protected, and I’m working through the issue, as I can.
I wrote this article using Preview 14 of .NET MAUI. As I write this final bit, today, RC1 is now out. I’m happy that the tooling has improved, but, it’s still not perfect. For instance, I still have to drop to the command line, and manually edit the project file, just to build an MSIX file, for Windows deployment. To me, that’s something that should be built into Visual Studio. Also, the integration with the MAC continues to be difficult to manage. I suspect that’s never going to change, since the two computer systems are so different, and the two companies involved compete for the same market share.
Still, MAUI has improved substantially, and the framework is, overall, fun and easy to work with. Look for more MAUI articles as I get my feet wet with the framework, in the coming months.