QuickCrypto For .NET MAUI – Part 4

QuickCrypto For .NET MAUI – Part 4

In the last few articles we have walked through how to put together a complete .NET MAUI cryptography tool. Along the way, I have added code for most of the project folders, with the exception of the root folder, and a folder called “ViewModels”. I’ll tackle the latter in this article.

The code for the AboutPageViewModel class looks like this:

namespace CG.Tools.QuickCrypto.ViewModels;

/// <summary>
/// This class is the view-model for the <see cref="AboutPage"/> view.
/// </summary>
public class AboutPageViewModel : ViewModelBase<AboutPageViewModel>
{
    /// <summary>
    /// This property contains the title for the application.
    /// </summary>
    public string Title => $"About:  {GetType().Assembly.ReadTitle()}";

    /// <summary>
    /// This property contains the version for the application.
    /// </summary>
    public string Version => $"Version:  {GetType().Assembly.ReadFileVersion()}";

    /// <summary>
    /// This property contains the description for the application.
    /// </summary>
    public string Description => GetType().Assembly.ReadDescription();

    /// <summary>
    /// This property contains the copyright for the application.
    /// </summary>
    public string Copyright => GetType().Assembly.ReadCopyright();

    /// <summary>
    /// This constructor creates a new instance of the <see cref="AboutPageViewModel"/>
    /// class.
    /// </summary>
    /// <param name="appSettings">The application settings for the view-model.</param>
    /// <param name="logger">The logger for the view-model.</param>
    public AboutPageViewModel(
        IOptions<AppSettings> appSettings,
        ILogger<AboutPageViewModel> logger
        ) : base(appSettings, logger)
    {
        
    }
}

The first thing to note is that the AboutPageViewModel class derives from the ViewModelBase class. That class, ViewModelBase, is our common view-model base. We’ll look at that code shortly.

For now, notice that the AboutPageViewModel class has several properties for the information we want to display on the page. Notice also, that the properties all use the extension methods from the AssemblyExtensions class. We covered that class in an earlier article. That means we’re actually reading everything from the executable’s attributes. That’s pretty cool, since that means we won’t have to locate and manually change these values if/when anything changes.

The code for the AesPageViewModel looks like this:

namespace CG.Tools.QuickCrypto.ViewModels;

/// <summary>
/// This class is the view-model for the <see cref="AesPage"/> view.
/// </summary>
public class AesPageViewModel : CryptoViewModelBase<AesPageViewModel>
{
    /// <summary>
    /// This constructor creates a new instance of the <see cref="AesPageViewModel"/>
    /// class.
    /// </summary>
    /// <param name="appSettings">The application settings to use for the 
    /// view-model.</param>
    /// <param name="logger">The logger to use for the view-model.</param>
    public AesPageViewModel(
        IOptions<AppSettings> appSettings,
        ILogger<AesPageViewModel> logger
        ) : base(appSettings, logger)
    {

    }

    /// <inheritdoc/>
    protected override void OnEncrypt()
    {
        try
        {
            // Is there anything to encrypt?
            if (!string.IsNullOrEmpty(DecryptedText))
            {
                var encryptedBytes = new byte[0];

                // Get the password bytes.
                var passwordBytes = Encoding.UTF8.GetBytes(
                    _appSettings.Value.Keys.Password
                    );

                // Get the salt bytes.
                var saltBytes = Encoding.UTF8.GetBytes(
                    _appSettings.Value.Keys.Salt
                    );

                // Create the algorithm
                using (var alg = Aes.Create())
                {
                    // Set the block and key sizes.
                    alg.KeySize = 256;
                    alg.BlockSize = 128;

                    // Derive the ACTUAL crypto key.
                    var key = new Rfc2898DeriveBytes(
                        passwordBytes,
                        saltBytes,
                        (int)_appSettings.Value.Keys.Iterations
                        );

                    // Generate the key and salt with proper lengths.
                    alg.Key = key.GetBytes(alg.KeySize / 8);
                    alg.IV = key.GetBytes(alg.BlockSize / 8);

                    // Create the encryptor.
                    using (var enc = alg.CreateEncryptor(
                        alg.Key,
                        alg.IV
                        ))
                    {
                        // Create a temporary stream.
                        using (var stream = new MemoryStream())
                        {
                            // Create a cryptographic stream.
                            using (var cryptoStream = new CryptoStream(
                                stream,
                                enc,
                                CryptoStreamMode.Write
                                ))
                            {
                                // Create a writer
                                using (var writer = new StreamWriter(
                                    cryptoStream
                                    ))
                                {
                                    // Write the bytes.
                                    writer.Write(
                                        DecryptedText
                                        );
                                }

                                // Get the bytes.
                                encryptedBytes = stream.ToArray();
                            }
                        }
                    }
                }

                // Convert the bytes back to an encoded string.
                var encryptedValue = Convert.ToBase64String(
                    encryptedBytes
                    );

                // Update the UI.
                EncryptedText = encryptedValue;
            }
            else
            {
                // Nothing to decrypt!
                EncryptedText = "";
            }            
        }
        catch (Exception ex)
        {
            // Prompt the user.
            OnErrorRaised(
                "Failed to encrypt text!",
                ex
                );
        }
    }

    /// <inheritdoc/>
    protected override void OnDecrypt()
    {
        try
        {
            // Convert the encrypted value to bytes.
            var encryptedBytes = Convert.FromBase64String(
                EncryptedText ?? ""
                );

            // Get the password bytes.
            var passwordBytes = Encoding.UTF8.GetBytes(
                _appSettings.Value.Keys.Password
                );

            // Get the salt bytes.
            var saltBytes = Encoding.UTF8.GetBytes(
                _appSettings.Value.Keys.Salt
                );

            var plainValue = "";

            // Create the algorithm
            using (var alg = Aes.Create())
            {
                // Set the block and key sizes.
                alg.KeySize = 256;
                alg.BlockSize = 128;

                // Derive the ACTUAL crypto key.
                var key = new Rfc2898DeriveBytes(
                    passwordBytes,
                    saltBytes,
                    (int)_appSettings.Value.Keys.Iterations
                    );

                // Generate the key and salt with proper lengths.
                alg.Key = key.GetBytes(alg.KeySize / 8);
                alg.IV = key.GetBytes(alg.BlockSize / 8);

                // Create the decryptor.
                using (var dec = alg.CreateDecryptor(
                    alg.Key,
                    alg.IV
                    ))
                {
                    // Create a temporary stream.
                    using (var stream = new MemoryStream(
                        encryptedBytes
                        ))
                    {
                        // Create a crypto stream.
                        using (var cryptoStream = new CryptoStream(
                            stream,
                            dec,
                            CryptoStreamMode.Read
                            ))
                        {
                            using (var reader = new StreamReader(
                                cryptoStream
                                ))
                            {
                                plainValue = reader.ReadToEnd();
                            }
                        }
                    }
                }
            }

            // Update the UI.
            DecryptedText = plainValue;
        }
        catch (Exception ex)
        {
            // Prompt the user.
            OnErrorRaised(
                "Failed to decrypt text!",
                ex
                );
        }
    }
}

The first thing to note is that the AesPageViewModel class derives from the CryptoViewModelBase class. We’ll cover that class shorty. For now, we’ll focus on the AesPageViewModel class.

There are two methods: OnEncrypt and OnDecrypt. Let’s cover OnEncrypt first. The method starts by checking the DecryptedText property for text. The DecryptedText property is inherited from the CryptoViewModelBase class. Assuming DecryptedText isn’t empty, we continue by converting the Password property, from the _appSettings field, into a byte array. The _appSettings field is inherited from the ViewModelBase. Next we convert the Salt property, on the _appSettings field, to a byte array.

Next we create an instance of the Aes class. Aes is a standard part of .NET and encapsulates the AES cryptographic algorithm. AES wants the key and block sizes to be a certain size so we set that up here, choosing a size of 256 for the Key, and 128 for the block.

Next, we create an instance of the Rfc2898DeriveBytes class, and feed those two byte arrays into it. Rfc2898DeriveBytes encapsulates the RFC2898 algorithm, which helps us create encryption keys that are much more secure than simply using a password string, in .NET.

Once the Rfc2898DeriveBytes class has generated our key, we feed that value (along with a IV value, generated from our SALT), into the AES instance we created earlier. At this point, we’re ready to begin the encryption process.

We start the encryption process by creating an encryptor, (actually an ICryptoTransform object) from the AES we created earlier. Next, we create a MemoryStream object to use as a working buffer. Next, we pass that buffer into a CryptoStream object, which, along with the ICryptoTransform object, will handle all the heavy lifting involved in the encryption itself. From the outside, CryptoStream is just a stream, like any other .NET stream, so we need to create a StreamWriter to work with it. We do that next. Once we’ve wrapped everything up nicely, we then write the value of the DecryptedText property, into the stream writer, and .NET literally handles everything else for us.

.NET writes the encrypted version of DecryptedText to our internal buffer. So, we save a reference to those bytes and convert those bytes to a Base64 encoded string, which we finally write into the EncryptedText property. EncryptedText is a property we inherited from the CryptoViewModelBase class.

The OnDecrypt method does the reverse of what we did in OnEncrypt. The key creation from the Password and Salt properties is the same. But, instead of creating an encryptor from the AES object, we create a decryptor instead. Also, instead of reading from the DecryptedText property, we read from the EncryptedText property. Also, instead of creating a StreamWriter, we create a StreamReader object, since we read to perform a decryption.

In the end, we wind up with the decrypted value of whatever was in the EncryptedText property, and we write that value into the DecryptedText property at the end of the method.

The code for the DataProtectionPageViewModel class looks like this:

namespace CG.Tools.QuickCrypto.ViewModels;

/// <summary>
/// This class is the view-model for the <see cref="DataProtectionPage"/> view.
/// </summary>
public class DataProtectionPageViewModel : CryptoViewModelBase<DataProtectionPageViewModel>
{
    /// <summary>
    /// This constructor creates a new instance of the <see cref="DataProtectionPageViewModel"/>
    /// class.
    /// </summary>
    /// <param name="appSettings">The application settings to use for the 
    /// view-model.</param>
    /// <param name="logger">The logger to use for the view-model.</param>
    public DataProtectionPageViewModel(
        IOptions<AppSettings> appSettings,
        ILogger<DataProtectionPageViewModel> logger
        ) : base(appSettings, logger)
    {

    }

    /// <inheritdoc/>
    protected override void OnEncrypt()
    {
        try
        {
            // Check for a missing cert.
            if (string.IsNullOrEmpty(_appSettings.Value.Certs.X509Pem))
            {
                // Warn the user.
                OnWarningRaised(
                    "A certificate wasn't provided in the settings, " +
                    "which means the keys won't be encrypted, at rest. " +
                    Environment.NewLine + Environment.NewLine + 
                    "To ensure the keys are protected, at rest, please " +
                    "provide a certificate, in the settings."
                    );
            }

            // Create a data-protector.
            var dataProtector = CreateDataProtector();

            // Is there anything to encrypt?
            if (!string.IsNullOrEmpty(DecryptedText))
            {
                // Protect the text.
                EncryptedText = dataProtector.Protect(DecryptedText ?? "");
            }
            else
            {
                // Nothing to encrypt!
                EncryptedText = "";
            }
        }
        catch (Exception ex)
        {
            // Prompt the user.
            OnErrorRaised(
                "Failed to encrypt text!",
                ex
                );
        }
    }

    /// <inheritdoc/>
    protected override void OnDecrypt()
    {
        try
        {
            // Create a data-protector.
            var dataProtector = CreateDataProtector();

            // Is there anything to decrypt?
            if (!string.IsNullOrEmpty(EncryptedText))
            {
                // Unprotect the text.
                DecryptedText = dataProtector.Unprotect(EncryptedText ?? "");
            }
            else
            {
                // Nothing to decrypt!
                DecryptedText = "";
            }                
        }
        catch (Exception ex)
        {
            // Prompt the user.
            OnErrorRaised(
                "Failed to decrypt text!",
                ex
                );
        }
    }

    /// <summary>
    /// This method creates a user configured data-protector.
    /// </summary>
    /// <returns>A <see cref="IDataProtector"/> instance.</returns>
    private IDataProtector CreateDataProtector()
    {
        // Get a path to our private folder.
        var appFolderPath = Path.Combine(
            Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
            "CodeGator",
            AppDomain.CurrentDomain.FriendlyName.Replace(".", "")
            );

        // Should we create the path?
        if (!Directory.Exists(appFolderPath))
        {
            // Make sure the path exists.
            Directory.CreateDirectory(appFolderPath);
        }

        // The reason we're creating the data protector here, instead of injecting
        //   it with the .NET DI container is becuase we need to be able to re-create
        //   the object each time the user changes any of the related settings.

        // Create the data-protector provider.
        var dataProtectionProvider = DataProtectionProvider.Create(
            new DirectoryInfo(appFolderPath),
            config =>
            {
                // Did the user provide us with an X509 certificate?
                if (!string.IsNullOrEmpty(_appSettings.Value.Certs.X509Pem))
                {
                    // Create the actual cert.
                    var cert = X509Certificate2.CreateFromPem(
                        _appSettings.Value.Certs.X509Pem.AsSpan()
                        );

                    // Make sure the cert is valid.
                    cert.Verify();

                    // Use the certificate for the ASP.NET data-protector.
                    config.ProtectKeysWithCertificate(cert);

                    // We don't want the keys changing underneath us.
                    config.DisableAutomaticKeyGeneration();
                }                
            });

        // Create the data-protector.
        var dataProtector = dataProtectionProvider.CreateProtector(
            AppDomain.CurrentDomain.FriendlyName
            );

        // Return the data-protector.
        return dataProtector;
    }
}

The DataProtectionPageViewModel class inherits from the CryptoViewModelBase class, which we’ll cover next.

The class has three methods: OnEncrypt, OnDecrypt, and CreateDataProtector. We’ll cover the CreateDataProtector method first.

The method starts by creating the path to the application’s appSettings.json file. We then use that path, along with any X509 certificate provided by the user, to create an IDataProtectionProvider object. We verify the certificate here, so if the value is wrong we should throw an exception to warn the user.

Once we have the IDataProtectionProvider object, we then use it to create the IDataProtector instance and return it.

The OnEncrypt method is similar to the one we looked at before, for the AES page’s view-model. However, this time we’re using the ASP.NET data protector, instead of the .NET AES class, to perform our encryption.

Since we already did the heavy lifting associated with creating a properly configured IDataProtector object, using the settings from our appSettings.json page, this version of OnEncrypt is much simpler than the one we looked at before. This version checks to see if there’s anything in the DecryptedText property. If so, it then calls the Protect method on the IDataProtector, and the results are then written to the EncryptedText property.

It really is that easy – see why so many people like to use the ASP.NET data protector?

The OnDecrypt method is similarly simple. This time, using the Unprotect method of the IDataProtector object to decrypt text from the EncryptedText property, and write the decrypted version in the DecryptedText property.

Both the AesPageViewModel and the DataProtectionPageViewModel classes derive from a common base class named CryptoViewModelBase. Here is the code for that class:

namespace CG.Tools.QuickCrypto.ViewModels;

/// <summary>
/// This class is a base implementation of a cryptographic view-model.
/// </summary>
public abstract class CryptoViewModelBase<T> : ViewModelBase<T>
    where T : CryptoViewModelBase<T>
{
    /// <summary>
    /// This field backs the <see cref="DecryptedText"/> property.
    /// </summary>
    private string? _decryptedText;

    /// <summary>
    /// This field backs the <see cref="EncryptedText"/> property.
    /// </summary>
    private string? _encryptedText;

    /// <summary>
    /// This property contains clear text to be encrypted.
    /// </summary>
    public string? DecryptedText
    {
        get { return _decryptedText; }
        set
        {
            _decryptedText = value;
            OnPropertyChanged();
        }
    }

    /// <summary>
    /// This property contains encrypted text to be decrypted.
    /// </summary>
    public string? EncryptedText
    {
        get { return _encryptedText; }
        set
        {
            _encryptedText = value;
            OnPropertyChanged();
        }
    }

    /// <summary>
    /// This command starts an encryption operation.
    /// </summary>
    public ICommand Encrypt { get; set; }

    /// <summary>
    /// This command starts an decryption operation.
    /// </summary>
    public ICommand Decrypt { get; set; }

    /// <summary>
    /// This command copies the encrypted text to the clipboard.
    /// </summary>
    public ICommand EncryptCopy { get; set; }

    /// <summary>
    /// This command clears the decrypted text.
    /// </summary>
    public ICommand DecryptClear { get; set; }

    /// <summary>
    /// This constructor creates a new instance of the <see cref="CryptoViewModelBase{T}"/>
    /// class.
    /// </summary>
    /// <param name="appSettings">The application settings to use for the 
    /// view-model.</param>
    /// <param name="logger">The logger to use for the view-model.</param>
    protected CryptoViewModelBase(
        IOptions<AppSettings> appSettings,
        ILogger<T> logger
        ) : base(appSettings, logger)
    {
        Encrypt = new Command(OnEncrypt);
        Decrypt = new Command(OnDecrypt);
        EncryptCopy = new Command(OnEncryptCopy);
        DecryptClear = new Command(OnDecryptClear);
    }

    /// <summary>
    /// This method performs an encryption operation.
    /// </summary>
    protected abstract void OnEncrypt();

    /// <summary>
    /// This method performs a decryption operation.
    /// </summary>
    protected abstract void OnDecrypt();

    /// <summary>
    /// This method performs an encrypted copy operation.
    /// </summary>
    protected virtual async void OnEncryptCopy()
    {
        // Copy the text to the clipboard.
        await Clipboard.Current.SetTextAsync(_encryptedText)
            .ConfigureAwait(false);
    }

    /// <summary>
    /// This method clears the encrypted text.
    /// </summary>
    protected virtual void OnDecryptClear()
    {
        // Clear the text.
        EncryptedText = "";
    }
}

As we can see, the class derives from the ViewModelBase class. We’ll cover that class next. The class contains two properties: DecryptedText and EncryptedText. Both properties are bound to controls on the UI. I put the properties here because they are common to both the crypto views. Looking at the property setters, we see the familiar pattern where we update the backing field, and then, fire the PropertyChanged event, so the associated view(s) will know that the UI probably needs to be updated.

CryptoViewModelBase also has the following commands: Encrypt, Decrypt, EncryptCopy, and DecryptClear. These commands are bound to controls, on the UI. They are also wired up to handlers that we’ll cover shortly.

The constructor, for the CryptoViewModelBase class, creates the various ICommand instances, and stores the results in the command properties. This is all pretty standard MVVM command ‘stuff’, and it’s how we ensure that, when a button is clicked on the UI, that a corresponding handler method is called, on our view-model.

Speaking of handler methods … The handlers for this class are mostly abstract, since it’s up to any derived class to decide how to perform encryptions, or decryptions. So, for that reason, the OnEncrypt and OnDecrypt methods are both marked here as abstract.

The OnEncryptCopy method is called when the user wants to copy the encrypted text, on the UI, to the system’s clipboard. The OnDecryptClear method is called when the user wants to clear to lower half of the UI – which typically contains encrypted text.

That’s about it, for CryptoViewModelBase.

The code for the ViewModelBase class looks like this:

namespace CG.Tools.QuickCrypto.ViewModels;

/// <summary>
/// This class is a base implementation of a MAUI view-model.
/// </summary>
public abstract class ViewModelBase<T> : INotifyPropertyChanged
    where T : ViewModelBase<T>
{
    /// <summary>
    /// This field contains the shared application settings.
    /// </summary>
    internal protected readonly IOptions<AppSettings> _appSettings;

    /// <summary>
    /// This field contains the logger for the view-model.
    /// </summary>
    internal protected readonly ILogger<T> _logger;

    /// <summary>
    /// This event is fired whenever a property changes value.
    /// </summary>
    public event PropertyChangedEventHandler? PropertyChanged;

    /// <summary>
    /// This event is fired whenever an error occurs.
    /// </summary>
    public event ErrorRaisedEventHandler? ErrorRaised;

    /// <summary>
    /// This event is fired whenever a warning occurs.
    /// </summary>
    public event WarningRaisedEventHandler? WarningRaised;

    /// <summary>
    /// This property contains the caption for the application.
    /// </summary>
    public string Caption => AppDomain.CurrentDomain.FriendlyName;

    /// <summary>
    /// This constructor creates a new instance of the <see cref="ViewModelBase{T}"/>
    /// class.
    /// </summary>
    /// <param name="appSettings">The application settings to use for the 
    /// view-model.</param>
    /// <param name="logger">The logger to use for the view-model.</param>
    protected ViewModelBase(
        IOptions<AppSettings> appSettings,
        ILogger<T> logger
        )
    {
        // Save the reference(s).
        _appSettings = appSettings; 
        _logger = logger;
    }

    /// <summary>
    /// This method raises the <see cref="PropertyChanged"/> event.
    /// </summary>
    /// <param name="memberName">Not used - supplied by the compiler.</param>
    protected virtual void OnPropertyChanged(
        [CallerMemberName] string memberName = ""
        )
    {
        // Raise the event.
        PropertyChanged?.Invoke(
            this, 
            new PropertyChangedEventArgs(memberName)
            );
    }

    /// <summary>
    /// This method raises the <see cref="ErrorRaised"/> event.
    /// </summary>
    /// <param name="message">The message for the error.</param>
    /// <param name="ex">An optional exception.</param>
    protected virtual void OnErrorRaised(
        string message,
        Exception? ex
        )
    {
        // Raise the event.
        ErrorRaised?.Invoke(
            this,
            new ErrorRaisedArgs() 
            { 
                Message = message,
                Exception = ex
            });
    }

    /// <summary>
    /// This method raises the <see cref="WarningRaised"/> event.
    /// </summary>
    /// <param name="message">The message for the error.</param>
    protected virtual void OnWarningRaised(
        string message
        )
    {
        // Raise the event.
        WarningRaised?.Invoke(
            this,
            new WarningRaisedArgs()
            {
                Message = message
            });
    }
}

This is the base class for all our view-models. It implements the INotifyPropertyChanged interface, so we can alert view(s) whenever the internal state of our view-model(s) change, at runtime.

The class exposes the following events: PropertyChanged, ErrorRaised and WarningRaised. PropertyChanged is raised whenever the internal state of the view-model changes. ErrorRaised is raised whenever a view-model encounters an error that it wants to pass to the UI, for display. Similarly, WarningRaised is raised whenever a view-model encounters a warning that it wants to pass to the UI, for display.

The Caption property is there so any popups, on the UI, will have a predictable caption.

The constructor passed in an AppSettings, and ILogger instance, from the DI container. The AppSettings contains the application configuration settings. We covered that topic in an earlier article. The logger is just that, an object that writes to a log.

The OnPropertyChanged, OnErrorRaised, and OnWarningRaised methods are used to raise their corresponding events, at runtime.

That’s it for the ViewModelBase class.

The code for the SettingsPageViewModel class looks like this:

namespace CG.Tools.QuickCrypto.ViewModels;

/// <summary>
/// This class is the view-model for the <see cref="SettingsPage"/> view.
/// </summary>
public class SettingsPageViewModel : ViewModelBase<SettingsPageViewModel>
{
    /// <summary>
    /// This property contains the (optional) X509 PEM data for the ASP.NET 
    /// data-provider.
    /// </summary>
    public string X509Pem
    {
        get { return _appSettings.Value.Certs.X509Pem ?? ""; }
        set
        {
            _appSettings.Value.Certs.X509Pem = value;
            OnPropertyChanged();
        }
    }

    /// <summary>
    /// This property contains the password for .NET crypto algorithms.
    /// </summary>
    public string Password
    {
        get { return _appSettings.Value.Keys.Password; }
        set
        {
            _appSettings.Value.Keys.Password = value;
            OnPropertyChanged();
        }
    }

    /// <summary>
    /// This property contains the SALT for .NET crypto algorithms.
    /// </summary>
    public string Salt
    {
        get { return _appSettings.Value.Keys.Salt; }
        set
        {
            _appSettings.Value.Keys.Salt = value;
            OnPropertyChanged();
        }
    }

    /// <summary>
    /// This property contains the Rfc2898 iterations for .NET crypto algorithms.
    /// </summary>
    public double Iterations
    {
        get { return _appSettings.Value.Keys.Iterations; }
        set
        {
            if (0 >= value)
            {
                _appSettings.Value.Keys.Iterations = 1;
            }
            else if (50000 < value)
            {
                _appSettings.Value.Keys.Iterations = 50000;
            }
            else
            {
                _appSettings.Value.Keys.Iterations = value;
            }
            OnPropertyChanged();
            OnPropertyChanged(nameof(IterationsLabel));
        }
    }

    /// <summary>
    /// This property contains the iterations label .NET crypto algorithms.
    /// </summary>
    public string IterationsLabel
    {
        get { return $"RFC 2898 Iterations: ({_appSettings.Value.Keys.Iterations:N0})"; }
    }

    /// <summary>
    /// This constructor creates a new instance of the <see cref="SettingsPageViewModel"/>
    /// class.
    /// </summary>
    /// <param name="appSettings">The application settings for the view-model.</param>
    /// <param name="logger">The logger for the view-model.</param>
    public SettingsPageViewModel(
        IOptions<AppSettings> appSettings,
        ILogger<SettingsPageViewModel> logger
        ) : base(appSettings, logger)
    {
        
    }

    /// <inheritdoc/>
    protected override void OnPropertyChanged(
        [CallerMemberName] string memberName = ""
        )
    {
        // Ignore label changes.
        if (nameof(IterationsLabel) == memberName)
        {
            return;
        }

        // Write the changes to disk.
        _appSettings.Value.WriteToDisk();

        // Give the base class a chance.
        base.OnPropertyChanged(memberName);
    }
}

As shown above, the class derives from the ViewModelBase class. It contains a property for each of the application settings. So, X509Pem, Password, Salt, and Iterations . IterationsLabel is, technically, not an application settings but it does make it easier to know how many iterations have been selected on the slider control we bound to the Iterations property.

The OnPropertyChanged method, from ViewModelBase, is overridden, so that we can write the updated AppSettings value(s) to the disk, whenever a change is made. For that purpose, we call the WriteToDisk extension method, using the _appSettings instance we got from the DI container. We covered WriteToDisk in an earlier article.

That’s it for the SettingsPageViewModel class.

The very last view-model is called AppShellViewModel, and it looks like this:

namespace CG.Tools.QuickCrypto.ViewModels;

/// <summary>
/// This class is the view-model for the <see cref="AppShell"/> view.
/// </summary>
public class AppShellViewModel : ViewModelBase<AppShellViewModel>
{
    /// <summary>
    /// This constructor creates a new instance of the <see cref="AppShellViewModel"/>
    /// class.
    /// </summary>
    /// <param name="appSettings">The application settings to use for the 
    /// view-model.</param>
    /// <param name="logger">The logger to use for the view-model.</param>
    public AppShellViewModel(
        IOptions<AppSettings> appSettings,
        ILogger<AppShellViewModel> logger
        ) : base(appSettings, logger)
    {
        
    }
}

This class derives from ViewModelBase, and serves as a placeholder, in case I ever need a view-model for the application shell.

With all the code we covered, this article has gotten longer than I like. So, for that reason, I’ll cover the last few class in the next article, and wrap everything up for CG.Tools.QuickCrypto. See you then!

Photo by Adi Goldstein on Unsplash