In the last article, I wrote about logging in C# and how to use log formatters to suit your needs. Once you start adding logs to your applications, you start to notice there is too much info that you must add to every message: the event id, level, scopes, besides the message (which you can even add parameters, if you also want to do some structured logging).

When the project starts to grow, some issues with the logging appear: you cannot enforce standards and you may miss some messages (Did you add the correct event id to the message? Should it be a trace or an info message?) when processing the logs.

Fortunately, Microsoft introduced Compile-time logging source code generation. With this, you can customize your log messages and ease the task of adding logs.

The generated code will use the LoggerMessage.Define functionality to generate code at compile time that is much faster than run time approaches.

To use the source code generator, all you have to do is to have a partial class with a partial method, decorated with the LoggerMessage attribute. With that, the implementation for the partial method will be generated by the compiler.

In this article, we will take the project created in this article and add some code generated logging to it. This project can be downloaded at https://github.com/bsonnino/CustomerService.

Once we download and run it, we can open Swagger to test it with https://localhost:7191/swagger/index.html (the port number can change). In the console window, you will see something like this:

Now, we will start to customize our log messages. For that, we will create a new partial static class and will name it LogExtensions. In this class, we will add a partial method named ShowRetrievedCustomerCount and will decorate it with the LoggerMessage attribute:

public  static partial class LoggerExtensions
{
    [LoggerMessage(EventId = 100, Level = LogLevel.Information, 
        Message = "{methodName}({lineNumber}) - Retrieved {Count} customers")]
    public static partial void ShowRetrievedCustomerCount(this ILogger logger, 
        int count, [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);
}

And that’s all we need for now. Some notes, here:

  • I’m passing the ILogger instance as a parameter for the function. That’s needed for the source code generation work. It doesn’t need to be the first parameter, but it must be present.
  • I used the keyword this for the ILogger parameter. This is not mandatory, but I wanted to make this method as an extension method, so it can be called with logger.ShowRetrievedCustomerCount(10)
  • The method must be void
  • I am passing the method name and line number, obtained with the attributes CallerMemberName and CallerLineNumber

With that, we can use our new function for the logging:

app.MapGet("/customers", async (CustomerDbContext context) =>
{
    var customers = await context.Customer.ToListAsync();
    logger.ShowRetrievedCustomerCount(customers.Count);
    return Results.Ok(customers);
});

When we run the code and test it, we get:

As you can see, the message is output to the console, with level Information and event id 100. It is a structured message (the customer count is a variable of the message) and it has the source method name and line.

We can change the format of the messages and remove the Entity Framework messages by changing the appsettings.json file to:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Warning"
    },
    "Console": {
      "FormatterName": "simple",
      "FormatterOptions": {
        "SingleLine": true,
        "IncludeScopes": false,
        "TimestampFormat": "yyyy-MM-dd HH:mm:ss zzz ",
        "ColorBehavior": "enabled",
        "JsonWriterOptions": {
          "Indented": true
        }
      }
    }
  },
  "AllowedHosts": "*"
}

We set the formatting to single line and add the timestamp to get something like this:

NOTE – When I changed the appsettings file, I saw that the messages that came from the app were properly formatted and the ones that I had introduced were not. That’s because, in the original code, I have created a different logger that doesn’t get its messages for appsettings.json (the default logger created by the Asp.NET app does that automatically). The simplest and cleanest solution was to use the default logger, by changing this code:

var logger = LoggerFactory.Create(config =>
    {
        config.AddConsole();
    }).CreateLogger("CustomerApi");

with this one:

var logger = app.Logger;

If we want more info for the log, we can set the formatter name to json and IncludeScopes to true:

As you can see, in the State object, you have the count, method name, line number and even the original format of the messages. In the Scopes object you have data about the message, connection id and even the request path for the request.

We can continue adding messages for logging:

using System.Runtime.CompilerServices;

public  static partial class LoggerExtensions
{
    [LoggerMessage(EventId = 100, Level = LogLevel.Information,
	 Message = "{methodName}({lineNumber}) - Retrieved {Count} customers")]
    public static partial void ShowRetrievedCustomerCount(this ILogger logger, int count, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);

    [LoggerMessage(EventId = 404, Level = LogLevel.Information,
	 Message = "{methodName}({lineNumber}) - Customer {customerId} not found.")]
    public static partial void CustomerNotFound(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);
    
    [LoggerMessage(EventId = 200, Level = LogLevel.Trace,
	 Message = "{methodName}({lineNumber}) - Retrieved customer {customerId}")]
    public static partial void RetrievedCustomer(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);

    [LoggerMessage(EventId = 300, Level = LogLevel.Information,
	 Message = "{methodName}({lineNumber}) - Creating customer {customerId}")]
    public static partial void CreatingCustomer(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);

    [LoggerMessage(EventId = 302, Level = LogLevel.Trace,
	 Message = "{methodName}({lineNumber}) - Created customer {customerId}")]
    public static partial void CreatedCustomer(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);

    [LoggerMessage(EventId = 400, Level = LogLevel.Information,
	 Message = "{methodName}({lineNumber}) - Updating customer {customerId}")]
    public static partial void UpdatingCustomer(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);

    [LoggerMessage(EventId = 402, Level = LogLevel.Trace,
	 Message = "{methodName}({lineNumber}) - Updated customer {customerId}")]
    public static partial void UpdatedCustomer(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);

    [LoggerMessage(EventId = 500, Level = LogLevel.Information,
	 Message = "{methodName}({lineNumber}) - Deleting customer {customerId}")]
    public static partial void DeletingCustomer(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);

    [LoggerMessage(EventId = 502, Level = LogLevel.Trace,
	 Message = "{methodName}({lineNumber}) - Deleted customer {customerId}")]
    public static partial void DeletedCustomer(this ILogger logger, string customerId, 
	 [CallerMemberName] string methodName = "", [CallerLineNumber] int lineNumber = 0);
}

As you can see we have some messages who have the Information level and others that have the Trace level. We can add now the log messages in the code:

using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddDbContext<CustomerDbContext>();

var app = builder.Build();

app.UseSwagger();
app.UseSwaggerUI();

var logger = app.Logger;

using (var scope = app.Services.CreateScope())
{
    var dbContext = scope.ServiceProvider.GetRequiredService<CustomerDbContext>();
    await dbContext.Database.EnsureCreatedAsync();
}

app.MapGet("/customers", async (CustomerDbContext context) =>
{
    var customers = await context.Customer.ToListAsync();
    logger.ShowRetrievedCustomerCount(customers.Count);
    return Results.Ok(customers);
});

app.MapGet("/customers/{id}", async (string id, CustomerDbContext context) =>
{
    var customer = await context.Customer.FindAsync(id);
    if (customer == null)
    {
        logger.CustomerNotFound(id);
        return Results.NotFound();
    }
    logger.RetrievedCustomer(id);
    return Results.Ok(customer);
});

app.MapPost("/customers", async (Customer customer, CustomerDbContext context) =>
{
    logger.CreatingCustomer(customer.Id);
    context.Customer.Add(customer);
    await context.SaveChangesAsync();
    logger.CreatedCustomer(customer.Id);
    return Results.Created($"/customers/{customer.Id}", customer);
});

app.MapPut("/customers/{id}", async (string id, Customer customer, CustomerDbContext context) =>
{
    logger.UpdatingCustomer(id);
    var currentCustomer = await context.Customer.FindAsync(id);
    if (currentCustomer == null)
    {
        logger.CustomerNotFound(id);
        return Results.NotFound();
    }

    context.Entry(currentCustomer).CurrentValues.SetValues(customer);
    await context.SaveChangesAsync();
    logger.UpdatedCustomer(id);
    return Results.NoContent();
});

app.MapDelete("/customers/{id}", async (string id, CustomerDbContext context) =>
{
    logger.DeletingCustomer(id);
    var currentCustomer = await context.Customer.FindAsync(id);
    if (currentCustomer == null)
    {
        logger.CustomerNotFound(id);
        return Results.NotFound();
    }

    context.Customer.Remove(currentCustomer);
    await context.SaveChangesAsync();
    logger.DeletedCustomer(id);
    return Results.NoContent();
});

app.Run();

After changing the appsettings file to show the trace messages and give a more succint report:

{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning",
      "Microsoft.EntityFrameworkCore": "Warning",
      "CustomerService": "Trace"
    },
    "Console": {
      "FormatterName": "simple",
      "FormatterOptions": {
        "SingleLine": true,
        "IncludeScopes": false,
        "ColorBehavior": "enabled",
        "JsonWriterOptions": {
          "Indented": true
        }
      }
    }
  },
  "AllowedHosts": "*"
}

We get this:

As you can see, we have standard messages, with the correct event ids and levels and we have the methods and line numbers, without having to bother with message formatting and setting values for the event ids or levels.

The full source code for this article is at https://github.com/bsonnino/EnhancedLogging

Introduction

Logging is an essential aspect of software development that enables us to track and understand the behavior of our applications. In fact, there are many logging frameworks to help with this task. In this post, I’ve shown how to use Serilog to generate structured logging.

In .NET 5.0, Microsoft introduced a new feature for logging, log formatters. Prior to that, the logging system only supported a single log format, which was plain text: you could only create logs using plain text and, if you wanted something different, you had to format the data by yourself. The introduction of logging formatters made it possible to format log messages in different formats, such as JSON and SystemD (it also allowed developers to create their own custom formatters).

In this blog post, we will show log formatters, exploring their purpose, implementation, examples on how to use and create formatters that suit your needs.

What are Log Formatters?

Log formatters are components responsible for formatting log messages in a specific way. They take raw log data, such as timestamps, log levels, and message details, and transform them into a human-readable format. Log formatters greatly help in the analysis and comprehension of log entries, making troubleshooting and debugging easier.

Predefined log formatters

The .NET team has implemented three predefined log formatters:

  • Simple – With this formatter, you can add time and log level in each log message, and also use ANSI coloring and indentation of messages.
  • JSON – This formatter generates the log in a json format.
  • SystemD – This formatter allows to use the Syslog format, available in containers, does not color the messages and always logs the messages in a single line.

To show the usage of the formatters, we’ll create a console app that will use these new features to log data.

Create a new console app with these commands

dotnet new console -o PredefFormatters
cd PredefFormatters
code .

Add the packages Microsoft.Extensions.Logging and Microsoft.Extensions.Logging.Console with

dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Logging.Console

In VS Code, add this code in Program.cs:

using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddSimpleConsole();
});

var logger = loggerFactory.CreateLogger<Program>();

logger.LogTrace("This is a trace message");
logger.LogDebug("This is a debug message");
logger.LogInformation("This is an information message");
logger.LogWarning("This is a warning message");
logger.LogError("This is an error message");
logger.LogCritical("This is a critical message");

When you run it, you will get something like this:

At this point, you must be puzzled, asking yourself what’s happened with the other messages. The answer is that the logger is caching the messages, and the program finishes before it can show all messages, thus you can only see one message. You can see a detailed answer here.

The solution in this case is to add a delay at the end to wait all the messages to be shown:

using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddSimpleConsole();
});

var logger = loggerFactory.CreateLogger<Program>();

logger.LogTrace("This is a trace message");
logger.LogDebug("This is a debug message");
logger.LogInformation("This is an information message");
logger.LogWarning("This is a warning message");
logger.LogError("This is an error message");
logger.LogCritical("This is a critical message");
Task.Delay(1000).Wait();

At the end, I’m adding a delay of 1 second to flush all messages. Usually you won’t need this hack, but that’s good to know.

EDIT – After publishing the article, Thomas (see comment below) pointed me a cleaner way to flush the log queue: just dispose the logger factory. This is a cleaner way to do it and should be used, instead of the Delay. In this case, the code will be like this:

using Microsoft.Extensions.Logging;

// With this notation, we don't need the braces, the logger factory will be disposed at the end
// Feel free to use the braces if you feel that it's more explicit
using var loggerFactory = LoggerFactory.Create(builder =>
{
    builder.AddSimpleConsole();
});

var logger = loggerFactory.CreateLogger<Program>();

logger.LogTrace("This is a trace message");
logger.LogDebug("This is a debug message");
logger.LogInformation("This is an information message");
logger.LogWarning("This is a warning message");
logger.LogError("This is an error message");
logger.LogCritical("This is a critical message");

If you notice the output of the program:

You can see that there are still two messages missing: the Trace and Debug messages. That’s because the default level for logging is Information and, without any configuration, you you won’t be able to see these messages. One way to configure logging is by changing the builder configuration to set the new level:

using Microsoft.Extensions.Logging;

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddSimpleConsole()
        .SetMinimumLevel(LogLevel.Trace);
});

var logger = loggerFactory.CreateLogger<Program>();

logger.LogTrace("This is a trace message");
logger.LogDebug("This is a debug message");
logger.LogInformation("This is an information message");
logger.LogWarning("This is a warning message");
logger.LogError("This is an error message");
logger.LogCritical("This is a critical message");
Task.Delay(1000).Wait();

If you run the program again, you will get the missing messages. Although this is a good way to configure logging, you have to recompile the code every time you want to change something. In this case, there is a better way to do it, using a configuration file.

For that we must follow the steps shown at this article and add the packages Microsoft.Extensions.Configuration and Microsoft.Extensions.Configuration.Json:

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Json

And add this code to add the configuration:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile($"appsettings.json", false, true)
    .Build();
                                
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddSimpleConsole();
});

var logger = loggerFactory.CreateLogger<Program>();

logger.LogTrace("This is a trace message");
logger.LogDebug("This is a debug message");
logger.LogInformation("This is an information message");
logger.LogWarning("This is a warning message");
logger.LogError("This is an error message");
logger.LogCritical("This is a critical message");
Task.Delay(1000).Wait();

Once you do this and add an appsettings.json file to the folder with this content:

{
    "Logging": {
        "LogLevel": {
            "Default": "Trace"
        }
    }
}

You will be able to see all messages. As you can see from the output, every message is shown in two lines: the first one shows the category (Program, that was used in the CreateLogger method), and the other has the message. If we want everything in a single line, we can add this configuration:

{
    "Logging": {
        "LogLevel": {
            "Default": "Trace"
        },
        "Console": {
            "FormatterOptions": {
                "SingleLine": true
            }
        }
    }
}

This will cause the output to be in a single line:

We can even change the log formatting in the configuration, but for that, we need to change the line .AddSimpleConsole(); to .AddConsole(); and change the Console section in appsettings to

"Console": {
    "FormatterName": "simple",
    "FormatterOptions": {
        "SingleLine": true
    }
}

Now, we can change the FormatterName setting to json or systemd and have different formatting for the log:

As you can see, we can change the log formats by changing the configuration file. We can add more configurations for the logs, by adding new options:

"Console": {
    "FormatterName": "simple",
    "FormatterOptions": {
        "SingleLine": true, 
        "IncludeScopes": true,
        "TimestampFormat": "yyyy-MM-dd HH:mm:ss zzz",
        "ColorBehavior": "disabled"
    }
}

"Console": {
    "FormatterName": "json",
    "FormatterOptions": {
        "SingleLine": true, 
        "IncludeScopes": true,
        "TimestampFormat": "yyyy-MM-dd HH:mm:ss zzz",
        "ColorBehavior": "disabled",
        "JsonWriterOptions": {
            "Indented": true
        }
    }
}

You may be asking what does the number [0] besides the category mean. This is the event Id, a struct that has the properties Id and Name, to identify the types of events. When you do something like this:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

EventId AppLogEvent = new EventId(100, "AppLog");
EventId DbLogEvent = 200;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile($"appsettings.json", false, true)
    .Build();
                                
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var logger = loggerFactory.CreateLogger<Program>();

logger.LogTrace(AppLogEvent,"This is a trace message");
logger.LogDebug(DbLogEvent,"This is a debug message");
logger.LogInformation(300,"This is an information message");
logger.LogWarning("This is a warning message");
logger.LogError("This is an error message");
logger.LogCritical("This is a critical message");
Task.Delay(1000).Wait();

As you can see, you can create EventIds explicitly, use the implicit conversion from an int or use an int directly as the first parameter for the log, and the log will show the id numbers instead of the [0]:

Scopes

You can also group the messages by using Scopes. A scope is a class that implements IDisposable and is created by the BeginScope method. It remains active until it’s disposed. Scopes can be nested.

For example, we could create scopes in our program with this code:

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;

var configuration = new ConfigurationBuilder()
    .SetBasePath(Directory.GetCurrentDirectory())
    .AddJsonFile($"appsettings.json", false, true)
    .Build();
                                
var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole();
});

var logger = loggerFactory.CreateLogger<Program>();

using(var scope1 = logger.BeginScope("Scope 1"))
{
    logger.LogInformation("This is an information message in scope 1");
    logger.LogWarning("This is a warning message in scope 1");
    logger.LogError("This is an error message in scope 1");
    logger.LogCritical("This is a critical message in scope 1");
}
using(var scope2 = logger.BeginScope("Scope 2"))
{
    logger.LogInformation("This is an information message in scope 2");
    logger.LogWarning("This is a warning message in scope 2");
    logger.LogError("This is an error message in scope 2");
    logger.LogCritical("This is a critical message in scope 2");
    using(var scope3 = logger.BeginScope("Scope 3"))
    {
        logger.LogInformation("This is an information message in scope 3");
        logger.LogWarning("This is a warning message in scope 3");
        logger.LogError("This is an error message in scope 3");
        logger.LogCritical("This is a critical message in scope 3");
    }
}
logger.LogTrace("This is a trace message");
logger.LogDebug("This is a debug message");
logger.LogInformation("This is an information message");
logger.LogWarning("This is a warning message");
logger.LogError("This is an error message");
logger.LogCritical("This is a critical message");
Task.Delay(1000).Wait();

Creating a custom formatter

In addition to using the available formatters, you can create your own formatter that formats the data according to your specific needs. For that, you must create a new class that inherits from ConsoleFormatter and use it to generate the data. Create a new file and name it CsvLogFormatter.cs. Add this code to create the formatter:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;

public class CsvLogFormatter : ConsoleFormatter
{
    public CsvLogFormatter() : base("CsvFormatter")
    {
    }

    public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider,     TextWriter textWriter)
    {
        string? message =
            logEntry.Formatter?.Invoke(
                logEntry.State, logEntry.Exception);

        if (message is null)
        {
            return;
        }
        var scopeStr = "";
        if (scopeProvider != null)
        {
            var scopes = new List<string>();
            scopeProvider.ForEachScope((scope, state) => state.Add(scope?.ToString() ?? ""), scopes);
            scopeStr = string.Join("|", scopes);
        }
        var logMessage = $"\"{logEntry.LogLevel}\",\"{logEntry.Category}[{logEntry.EventId}]\"," +
            $"\"{scopeStr}\",\"{message}\"";
        textWriter.WriteLine(logMessage);
    }
}

The formatter inherits from ConsoleFormatter and overrides the Write method, that receives the log entry, the scope provider (which will proved the scopes for the message) and the TextWriter, used for the log output. This method will format the message received, then it will obtain the scopes and join them in a string separated by “|” and then set the complete log message that will be written by the text writer.

This class works fine and outputs each log message as a line of comma-separated values, where every field is enclosed by quotes and the fields are separated by commas. To use it, we must set it up in Program.cs by calling AddConsoleFormatter within the ConfigureLogging method, as shown in the code snippet below:

var loggerFactory = LoggerFactory.Create(builder =>
{
    builder
        .AddConfiguration(configuration.GetSection("Logging"))
        .AddConsole()
        .AddConsoleFormatter<CsvLogFormatter, ConsoleFormatterOptions>();
});

As you can see, the ConsoleFormatterOptions is the second parameter in the generic call, which is automatically provided by the logging framework. However, there is currently no mention of these options in the formatter code. To use options in the code, we need to enhance the code, but, before that, we will create an options class specific for this formatter. One thing that must be added is the list separator character, which we will add in the CsvFormatterOptions class.

public sealed class CsvFormatterOptions : ConsoleFormatterOptions
{
    public string? ListSeparator { get; set; }
}

This class inherit from the ConsoleFormatterOptions class and introduces a new property called ListSeparator. To use it, we must enhance our formatter like this:

public class CsvLogFormatter : ConsoleFormatter, IDisposable
{
    CsvFormatterOptions? _options;
    private readonly IDisposable? _optionsReloadToken;

    public CsvLogFormatter(IOptionsMonitor<CsvFormatterOptions> options) : base("CsvFormatter")
    {
        _options = options.CurrentValue;
        _optionsReloadToken = options.OnChange(ReloadLoggerOptions);
    }

    private void ReloadLoggerOptions(CsvFormatterOptions currentValue)
    {
        _options = currentValue;
    }

    public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
    {
        string? message =
            logEntry.Formatter?.Invoke(
                logEntry.State, logEntry.Exception);

        if (message is null)
        {
            return;
        }
        var scopeStr = "";
        if (_options?.IncludeScopes == true && scopeProvider != null)
        {
            var scopes = new List<string>();
            scopeProvider.ForEachScope((scope, state) => state.Add(scope?.ToString() ?? ""), scopes);
            scopeStr = string.Join("|", scopes);
        }
        var listSeparator = _options?.ListSeparator ?? ",";
        if (_options?.TimestampFormat != null)
        {
            var timestampFormat = _options.TimestampFormat;
            var timestamp = "\"" +DateTime.Now.ToLocalTime().ToString(timestampFormat, 
                CultureInfo.InvariantCulture) + "\"";
            textWriter.Write(timestamp);
            textWriter.Write(listSeparator);
        }
        var logMessage = $"\"{logEntry.LogLevel}\"{listSeparator}"+
            $"\"{logEntry.Category}[{logEntry.EventId}]\"{listSeparator}" +
            $"\"{scopeStr}\"{listSeparator}\"{message}\"";
        textWriter.WriteLine(logMessage);
    }

    public void Dispose() => _optionsReloadToken?.Dispose();
}

Now the class also implements the IDisposable interface. The implementation of IDisposable is necessary because the constructor of CsvLogFormatter receives an IOptionsMonitor<CsvFormatterOptions> that registers an event handler for detecting changes in the options. By implementing IDisposable, we can ensure the proper disposal of the options event handler. This instance also has the current options value, that we use to initialize the options.

Since our options inherit from ConsoleFormatterOptions, we have access to all properties in the parent class, allowing us to utilize them for enhancing the output:

  • If IncludeScopes is true, we will gather the scopes and include them to the log entry
  • If TimestampFormat is defined, we will add the current timestamp to the output
  • We can use the ListSeparator option to change the list separator between the log values

All these options can still be configured in appsettings.json. For example, if we change the file to:

{
    "Logging": {
        "LogLevel": {
            "Default": "Trace"
        },
        "Console": {
            "FormatterName": "CsvFormatter",
            "FormatterOptions": {
                "SingleLine": true,
                "IncludeScopes": true,
                "ColorBehavior": "enabled",
                "TimestampFormat": "yyyy-MM-dd HH:mm:ss.fff zzz",
                "JsonWriterOptions": {
                    "Indented": true
                },
                "ListSeparator": ";"
            }
        }
    }
}

And run the program, we will get something like this:

Conclusion:

As you can see, log formatters offer flexibility, and you can tailor them to suit your specific requirements. You can change the output format for the data and even transform or filter it. That can improve a lot your logging experience.

All the source code for this project is at https://github.com/bsonnino/LogFormatters

One advantage of the new .NET Core (now .NET) apps is that they are cross platform. Once you create an app, the same code will run on Windows, Mac or Linux with no change. But there are some exceptions to this: WPF or WinForms apps are only for Windows. You cannot create a WPF app and run it on Linux, for example.

If you want to create a cross platform app, you will have to use another technology, like Blazor or MAUI. Blazor UI is based on Razor components and it’s not compatible with XAML, so it may be very difficult to convert. MAUI, on the other side, uses XAML and can be used to port your app to Mac, iOS or Android. But MAUI apps don’t run on Linux. If you want a Linux app, this is not the way. You can use Uno Platform (see my article here), it can run on the Web (as a WebAssembly), Linux, Mac or Windows, or you also have the option of using Avalonia UI.

In this article we will show how to convert an existing WPF app to Avalonia UI. We will use the project described in my MVVM Community toolkit 8.0 article. This project has some interesting features to explore:

  • It uses a .NET Standard library for the repository
  • It has a Datagrid to display the data
  • It uses the MVVM pattern and the MVVM Community toolkit NuGet package

Avalonia UI is an open source cross platform framework for .NET to develop cross platform apps using XAML. To use it, you need to install the Avalonia project templates with:

dotnet new install Avalonia.Templates

Once you do it, you can create a new basic project with:

dotnet new avalonia.app -o BasicApp
cd BasicApp
dotnet run

When you run it, you will see a basic app:

The generated code has these differences:

  • The extension for the view files is axaml instead of xaml (and the code behind extension is axaml.cs)
  • The default namespace for the view files is https://github.com/avaloniaui instead of http://schemas.microsoft.com/winfx/2006/xaml/presentation
  • The project includes the Avalonia, Avalonia.Desktop, Avalonia.Diagnostics and XamlNameReferenceGenerator NuGet packages
  • The project targets net6.0 (or net7.0), instead of net6.0-windows
  • There is no need to include the UseWPF clause in the project file
  • The Program.cs file and Main method are explicit
  • There is some initialization code in App.axaml.cs

Apart from that, designing the UI and the C# code aren’t much different from the standard WPF app.

For this app, we will use Visual Studio 2022 and the Avalonia Extension. This extension will provide all templates and a designer for the views. If you don’t want to use Visual Studio, you can use VS Code, but you won’t have the visual designer. In Visual Studio, go to Extensions/Manage Extensions and install the Avalonia for Visual Studio 2022 extension:

Let’s start converting our app. We have two approaches, here: convert our app in place, making changes in the files as needed, or create a new basic Avalonia project and add the features incrementally. I prefer to use the second approach, in this case all the basic infrastructure is already set and we can make sure that things are running while we are adding the features. In the in place conversion, it’s an all-or-nothing, and at the end we may not have any clue of what we’ve missed, in case it doesn’t run.

The first step is to clone our app from https://github.com/bsonnino/MVVMToolkit8.git. Then, we will create our app in Visual Studio:

That will create a new basic app. If you open MainWindow.axaml, you will see the code and the visual designer:

Let’s start converting our app. The first step is to add the two NuGet packages, CommunityToolkit.Mvvm and Microsoft.Extensions.DependencyInjection.

Then, copy the CustomerLib folder with all files to the folder of the Avalonia solution. We will use this project as is, as it’s a .NET Standard project and it can be used by Avalonia unchanged. In the solution explorer, add an existing project and select the CustomerLib.csproj file. That will add the lib to our solution. In the main project, add a project reference and add the CustomerLib project:

Then, copy the ViewModel folder to the project folder, it will appear in the solution explorer. Open MainViewModel.cs, you will see an error in ColletionViewSource:

That’s because the CollectionViewSource class doesn’t exist in Avalonia and we need to replace it with this code:

private readonly ICustomerRepository _customerRepository;
private Func<Customer, bool> _filter = c => true;
public IEnumerable<Customer> Customers => _customerRepository.Customers.Where(_filter);

[RelayCommand]
private void Search(string textToSearch)
{
    if (!string.IsNullOrWhiteSpace(textToSearch))
        _filter = c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
    else
        _filter = c => true;
    OnPropertyChanged(nameof(Customers));
}

Instead of using the WPF CollectionViewSource class, we are creating our filter and using it before displaying the data. Just to check, we can copy the Test project to the solution folder, add it to the current solution and run the tests to check. For that, we must do the following changes:

  • Change the Target Framework in the csproj file to .net6.0
  • Change the reference for the main project to MvvmAvalonia

Once we do that, we can compile the project, but we get the errors for the CollectionViewSource. For that, we must change the tests to:

[TestMethod]
public void SearchCommand_WithText_ShouldSetFilter()
{
    var customers = new List<Customer>
    {
        new Customer { Country = "a"},
        new Customer { Country = "text"},
        new Customer { Country = "b"},
        new Customer { Country = "texta"},
        new Customer { Country = "a"},
        new Customer { Country = "b"},
    };
    var repository = A.Fake<ICustomerRepository>();
    A.CallTo(() => repository.Customers).Returns(customers);
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("text");
    vm.Customers.Count().Should().Be(2);
}

[TestMethod]
public void SearchCommand_WithoutText_ShouldSetFilter()
{
    var customers = new List<Customer>
    {
        new Customer { Country = "a"},
        new Customer { Country = "text"},
        new Customer { Country = "b"},
        new Customer { Country = "texta"},
        new Customer { Country = "a"},
        new Customer { Country = "b"},
    };
    var repository = A.Fake<ICustomerRepository>();
    A.CallTo(() => repository.Customers).Returns(customers);
    var vm = new MainViewModel(repository);
    vm.SearchCommand.Execute("");
    vm.Customers.Count().Should().Be(6);
}

Now, when we run the tests, they all pass and we can continue. We will start adding the UI to the main window:

<Grid>
	<Grid.RowDefinitions>
		<RowDefinition Height="40" />
		<RowDefinition Height="*" />
		<RowDefinition Height="2*" />
		<RowDefinition Height="50" />
	</Grid.RowDefinitions>
	<StackPanel Orientation="Horizontal">
		<TextBlock Text="Country" VerticalAlignment="Center" Margin="5"/>
		<TextBox x:Name="searchText" VerticalAlignment="Center" Margin="5,3" Width="250" Height="25" VerticalContentAlignment="Center"/>
		<Button x:Name="PesqBtn" Content="Find" Width="75" Height="25" Margin="10,5" VerticalAlignment="Center"
                Command="{Binding SearchCommand}" CommandParameter="{Binding ElementName=searchText,Path=Text}"/>
	</StackPanel>
	<DataGrid AutoGenerateColumns="False" x:Name="master" CanUserAddRows="False" CanUserDeleteRows="True" Grid.Row="1"
              ItemsSource="{Binding Customers}" SelectedItem="{Binding SelectedCustomer, Mode=TwoWay}">
		<DataGrid.Columns>
			<DataGridTextColumn x:Name="customerIDColumn" Binding="{Binding Path=CustomerId}" Header="Customer ID" Width="60" />
			<DataGridTextColumn x:Name="companyNameColumn" Binding="{Binding Path=CompanyName}" Header="Company Name" Width="160" />
			<DataGridTextColumn x:Name="contactNameColumn" Binding="{Binding Path=ContactName}" Header="Contact Name" Width="160" />
			<DataGridTextColumn x:Name="contactTitleColumn" Binding="{Binding Path=ContactTitle}" Header="Contact Title" Width="60" />
			<DataGridTextColumn x:Name="addressColumn" Binding="{Binding Path=Address}" Header="Address" Width="130" />
			<DataGridTextColumn x:Name="cityColumn" Binding="{Binding Path=City}" Header="City" Width="60" />
			<DataGridTextColumn x:Name="regionColumn" Binding="{Binding Path=Region}" Header="Region" Width="40" />
			<DataGridTextColumn x:Name="postalCodeColumn" Binding="{Binding Path=PostalCode}" Header="Postal Code" Width="50" />
			<DataGridTextColumn x:Name="countryColumn" Binding="{Binding Path=Country}" Header="Country" Width="80" />
			<DataGridTextColumn x:Name="faxColumn" Binding="{Binding Path=Fax}" Header="Fax" Width="100" />
			<DataGridTextColumn x:Name="phoneColumn" Binding="{Binding Path=Phone}" Header="Phone" Width="100" />
		</DataGrid.Columns>
	</DataGrid>
	<customerApp:Detail Grid.Row="2" DataContext="{Binding SelectedCustomer}" Margin="5" x:Name="detail"/>
	<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Margin="5" Grid.Row="3">
		<Button Width="75" Height="25" Margin="5" Content="Add" Command="{Binding AddCommand}" />
		<Button Width="75" Height="25" Margin="5" Content="Remove" Command="{Binding RemoveCommand}" />
		<Button Width="75" Height="25" Margin="5" Content="Save" Command="{Binding SaveCommand}" />
	</StackPanel>
</Grid>

There is an error with the DataGrid. That’s because we need to add the package Avalonia.Controls.Datagrid. Once we add that, we can see some other errors:

  • The ItemsSource property has been changed to Items
  • The columns don’t have the Name field and should be removed
  • The CanUserAddRows and CanUserDeleteRows do not exist and should be removed
  • We should add the themes for the DataGrid in App.axaml:
<Application.Styles>
    <StyleInclude Source="avares://Avalonia.Themes.Default/DefaultTheme.xaml"/>
    <StyleInclude Source="avares://Avalonia.Themes.Default/Accents/BaseLight.xaml"/>
    <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Default.xaml"/>
</Application.Styles>

We can also see that this code is missing the Detail control. Add to the project a new item of type UserControl (Avalonia) and add the content from the original project:

<Grid>
    <Grid Name="grid1" >
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <Label Content="Customer Id:" Grid.Column="0" Grid.Row="0"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="0"   Margin="3" Name="customerIdTextBox" Text="{Binding Path=CustomerId, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Company Name:" Grid.Column="0" Grid.Row="1"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="1"   Margin="3" Name="companyNameTextBox" Text="{Binding Path=CompanyName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Contact Name:" Grid.Column="0" Grid.Row="2"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="2"   Margin="3" Name="contactNameTextBox" Text="{Binding Path=ContactName, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Contact Title:" Grid.Column="0" Grid.Row="3"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="3"   Margin="3" Name="contactTitleTextBox" Text="{Binding Path=ContactTitle, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Address:" Grid.Column="0" Grid.Row="4" HorizontalAlignment="Left" Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" Text="{Binding Path=Address, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center" />
        <Label Content="City:" Grid.Column="0" Grid.Row="5"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="5"   Margin="3" Name="cityTextBox" Text="{Binding Path=City, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Postal Code:" Grid.Column="0" Grid.Row="6"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="6"   Margin="3" Name="postalCodeTextBox" Text="{Binding Path=PostalCode, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Region:" Grid.Column="0" Grid.Row="7"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="7"   Margin="3" Name="regionTextBox" Text="{Binding Path=Region, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Country:" Grid.Column="0" Grid.Row="8"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="8"   Margin="3" Name="countryTextBox" Text="{Binding Path=Country, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Phone:" Grid.Column="0" Grid.Row="9"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="9"   Margin="3" Name="phoneTextBox" Text="{Binding Path=Phone, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
        <Label Content="Fax:" Grid.Column="0" Grid.Row="10"  Margin="3" VerticalAlignment="Center" />
        <TextBox Grid.Column="1" Grid.Row="10"   Margin="3" Name="faxTextBox" Text="{Binding Path=Fax, Mode=TwoWay, ValidatesOnExceptions=true, NotifyOnValidationError=true}" VerticalAlignment="Center"  />
    </Grid>
</Grid>

We must remove the , ValidatesOnExceptions=true, NotifyOnValidationError=true from the code, as it’s not available in Avalonia. Then, we should add the correct using clause in the main xaml:

xmlns:customerApp="using:MvvmAvalonia"

Once we do that and we run, we can see the UI (but not the data):

For the data, we must add the configuration for the services, in App.axaml.cs:

public partial class App : Application
{
    public override void Initialize()
    {
        AvaloniaXamlLoader.Load(this);
        Services = ConfigureServices();
    }

    public override void OnFrameworkInitializationCompleted()
    {
        if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
        {
            desktop.MainWindow = new MainWindow();
        }

        base.OnFrameworkInitializationCompleted();
    }

    public new static App Current => (App)Application.Current;

    public IServiceProvider Services { get; private set; }

    private static IServiceProvider ConfigureServices()
    {
        var services = new ServiceCollection();

        services.AddSingleton<ICustomerRepository, CustomerRepository>();
        services.AddSingleton<MainViewModel>();

        return services.BuildServiceProvider();
    }

    public MainViewModel MainVM => Services.GetService<MainViewModel>();
}

Then, we must set the DataContext on MainWindow.axaml.cs:

public MainWindow()
{
    InitializeComponent();
    DataContext = App.Current.MainVM;
}

Now, when we run the code, we can see it runs fine:

We’ve ported our WPF project to Avalonia, now it’s ready to be run on Linux. We’ll use WSLg (Windows Subsystem for Linux GUI) to run the app. Just open a Linux tab on terminal and cd to the project directory (the drive is mounted on /mnt/drive, like /mnt/c) and run the app with dotnet run. You should have something like this:

As you can see, porting a WPF app to Avalonia requires some changes, but most of the code is completely portable. If you want to ease the process, you can move the non-UI code to .NET Standard libraries and use them as-is. We’ve used the DataGrid and the MVVM Community Toolkit with no problems.

All the source code for the project is at https://github.com/bsonnino/MvvmAvalonia

Sometime ago, I wrote this article for the MSDN Magazine, about Aspect Oriented Programming and how it could solve cross-cutting concerns in your application, like:

  • Authentication
  • Logging
  • Data audit
  • Data validation
  • Data caching
  • Performance measuring

The article shows how to use the Decorator pattern and the RealProxy class to create a Dynamic Proxy to solve these issues in a simple manner. If you want to have a full introduction on the subject, I suggest that you take a look at the article.
The time has passed, things changed a lot and we are now close to the introduction of .NET 7. The RealProxy class doesn’t exist anymore, as it’s based in Remoting, which was not ported to .NET Core. Fortunately, we still have the System.Reflection.DispatchProxy class that can solve the problem.

With this class, we can still write proxies that decorate our classes and allow us to implement AOP in our programs. In this article, we will use the DispatchProxy class to create a dynamic proxy that allows us to implement a filter for the methods to be executed and execute other functions before and after the method execution.

In the command prompt, create a new console app with:

dotnet new console -o DynamicProxy
cd DynamicProxy
code .

In Program.cs, we will define a Customer record (put it at the end of the code):

record Customer(string Id, string Name, string Address);

Then, add a new Repository.cs file and add an IRepository interface in it:

public interface IRepository<T>
{
    void Add(T entity);
    void Delete(T entity);
    IEnumerable<T> GetAll();
}

The next step is to create the generic class Repository that implements this interface:

public class Repository<T> : IRepository<T> 
{
    private readonly List<T> _entities = new List<T>();
    public void Add(T entity)
    {
        _entities.Add(entity);
        Console.WriteLine("Adding {0}", entity);
    }

    public void Delete(T entity)
    {
        _entities.Remove(entity);
        Console.WriteLine("Deleting {0}", entity);
    }

    public IEnumerable<T> GetAll()
    {
        Console.WriteLine("Getting entities");
        foreach (var entity in _entities)
        {
            Console.WriteLine($"  {entity}");
        }
        return _entities;
    }
}

As you can see, our repository class is a simple class that will store the entities in a list, delete and retrieve them.

With this class created, we can add the code to use it in Program.cs:

Console.WriteLine("***\r\n Begin program\r\n");
var customerRepository = new Repository<Customer>();
var customer = new Customer(1, "John Doe", "1 Main Street");
customerRepository.Add(customer);
customerRepository.GetAll();
customerRepository.Delete(customer);
customerRepository.GetAll();
Console.WriteLine("\r\nEnd program\r\n***");

If you run this program, you will see something like this:

Now, let’s say we want to implement logging to this class, and have a log entry for every time it enters a method and another entry when it exits. We could do that manually, but it would be cumbersome to add logging before and after every method.

Using the DispatchProxy class, we can implement a proxy that will add logging to any class that implements the IRepository interface. Create a new file RepositoryLoggerProxy.cs and add this code:

using System.Reflection;

class RepositoryLogger<T> : DispatchProxy where T : class
{
    T? _decorated;

    public T? Create(T decorated)
    {
        var proxy = Create<T, RepositoryLogger<T>>() as RepositoryLogger<T>;
        if (proxy != null)
        {
            proxy._decorated = decorated;
        }
        return proxy as T;
    }


    protected override object? Invoke(MethodInfo? methodInfo, object?[]? args)
    {
        if (methodInfo == null)
        {
            return null;
        }

        Log($"Entering {methodInfo.Name}");
        try
        {
            var result = methodInfo.Invoke(_decorated, args);
            Log($"Exiting {methodInfo.Name}");
            return result;
        }
        catch
        {
            Log($"Error {methodInfo.Name}");
            throw;
        }
    }

    private static void Log(string msg)
    {
        Console.ForegroundColor = msg.StartsWith("Entering") ? ConsoleColor.Blue :
            msg.StartsWith("Exiting") ? ConsoleColor.Green : ConsoleColor.Red;
        Console.WriteLine(msg);
        Console.ResetColor();
    }
}

The RepositoryLogger class inherits from DispatchProxy and has a Create method that will create an instance of a class that implements the interface that’s decorated. When we call the methods of this class, they are intercepted by the overriden Invoke method and we can add the logging before and after executing the method.

To use this new class, we can use something like:

Console.WriteLine("***\r\n Begin program\r\n");
var customerRepository = new Repository<Customer>();
var customerRepositoryLogger = new RepositoryLogger<IRepository<Customer>>().Create(customerRepository);
if (customerRepositoryLogger == null)
{
    return;
}
var customer = new Customer(1, "John Doe", "1 Main Street");
customerRepositoryLogger.Add(customer);
customerRepositoryLogger.GetAll();
customerRepositoryLogger.Delete(customer);
customerRepositoryLogger.GetAll();
Console.WriteLine("\r\nEnd program\r\n***");

Now, running the code, we get:

We have logging entering and exiting the class without having to change it. Remove logging is as simple as changing one line of code.

With this knowledge, we can extend our proxy class to do any action we want. To add actions before, after and on error is just a matter of passing them in the creation of the proxy. We can create a DynamicProxy class with this code:

using System.Reflection;

class DynamicProxy<T> : DispatchProxy where T : class
{
    T? _decorated;
    private Action<MethodInfo>? _beforeExecute;
    private Action<MethodInfo>? _afterExecute;
    private Action<MethodInfo>? _onError;
    private Predicate<MethodInfo> _shouldExecute;

    public T? Create(T decorated, Action<MethodInfo>? beforeExecute, 
        Action<MethodInfo>? afterExecute, Action<MethodInfo>? onError, 
        Predicate<MethodInfo>? shouldExecute)
    {
        var proxy = Create<T, DynamicProxy<T>>() as DynamicProxy<T>;
        if (proxy == null)
        {
            return null;
        }
        proxy._decorated = decorated;
        proxy._beforeExecute = beforeExecute;
        proxy._afterExecute = afterExecute;
        proxy._onError = onError;
        proxy._shouldExecute = shouldExecute ?? (s => true);
        return proxy as T;
    }

    protected override object? Invoke(MethodInfo? methodInfo, object?[]? args)
    {
        if (methodInfo == null)
        {
            return null;
        }
        if (!_shouldExecute(methodInfo))
        {
            return null;
        }
        _beforeExecute?.Invoke(methodInfo);
        try
        {
            var result = methodInfo.Invoke(_decorated, args);
            _afterExecute?.Invoke(methodInfo);
            return result;
        }
        catch
        {
            _onError?.Invoke(methodInfo);
            throw;
        }
    }
}

In the Create method, we pass the actions we want to execute before after and on error after each method. We can also pass a predicate to filter the methods we don’t want to execute. To use this new class, we can do something like this:

Console.WriteLine("***\r\n Begin program\r\n");
var customerRepository = new Repository<Customer>();
var customerRepositoryLogger = new DynamicProxy<IRepository<Customer>>().Create(customerRepository,
    s => Log($"Entering {s.Name}"),
    s => Log($"Exiting {s.Name}"),
    s => Log($"Error {s.Name}"),
    s => s.Name != "GetAll");
if (customerRepositoryLogger == null)
{
    return;
}
var customer = new Customer(1, "John Doe", "1 Main Street");
customerRepositoryLogger.Add(customer);
customerRepositoryLogger.GetAll();
customerRepositoryLogger.Delete(customer);
customerRepositoryLogger.GetAll();
Console.WriteLine("\r\nEnd program\r\n***");

static void Log(string msg)
{
    Console.ForegroundColor = msg.StartsWith("Entering") ? ConsoleColor.Blue :
        msg.StartsWith("Exiting") ? ConsoleColor.Green : ConsoleColor.Red;
    Console.WriteLine(msg);
    Console.ResetColor();
}

Executing this code will show something like:

Note that, with this code, the method GetAll isn’t executed, as it was filtered by the predicate.

As you can see, this is a very powerful class, as it can implement many different aspects for any interface (the DispatchProxy class only works with interfaces). For example, if I want to create my own mocking framework, where I don’t execute any method of a class, I can change the code of the Invoke method to

protected override object? Invoke(MethodInfo? methodInfo, object?[]? args)
{
    if (methodInfo == null)
    {
        return null;
    }
    _beforeExecute?.Invoke(methodInfo);
    try
    {
        object? result = null;
        if (_shouldExecute(methodInfo))
        {
            result = methodInfo.Invoke(_decorated, args);
        }
        _afterExecute?.Invoke(methodInfo);
        return result;
    }
    catch
    {
        _onError?.Invoke(methodInfo);
        throw;
    }
}

And create the proxy with something like this:

var customerRepositoryLogger = new DynamicProxy<IRepository<Customer>>().Create(customerRepository,
    s => Log($"Entering {s.Name}"),
    s => Log($"Exiting {s.Name}"),
    s => Log($"Error {s.Name}"),
    s => false);

In this case, the real functions won’t be called, just the methods before and after the call:

As you can see, the DispatchProxy class allows the creation of powerful classes that add aspects to your existing classes, without having to change them. With the DynamicProxy class you just have to add the actions to execute and the filter for the functions to be executed.

All the source code for this article is at https://github.com/bsonnino/DynamicProxy

When you are developing a new project and need to store settings for it, the first thing that comes to mind is to use the Appsettings.json file. With this file, you can store all settings in a single file and restore them easily.

For example, let’s create a console project that has three settings: Id, Name and Version. In a command line prompt, type:

dotnet new console -o SecretStorage
cd SecretStorage
code .

This will open VS Code in the folder for the new project. Add a new file named appSettings.json and add this code in it:

{
    "AppData": {
        "Id": "IdOfApp",
        "Name": "NameOfApp",
        "Version": "1.0.0.0"
    }
}

We will add a AppData class in the project:

public class AppData
{
    public string Id { get; init; }
    public string Name { get; init; }
    public string Version { get; init; }

    public override string ToString()
    {
        return $"Id: {Id}, Name: {Name}, Version: {Version}";
    }
}

To read the settings into the class, we could use JsonDeserializer from System.Text.Json:

using System.Text.Json;

var settingsText = File.ReadAllText("appsettings.json");
var settings = JsonSerializer.Deserialize<Settings>(settingsText);
Console.WriteLine(settings);

You need to define a class Settings to read the data:

public class Settings
{
    public AppData AppData { get; set; }
}

That’s fine, but let’s say you have different settings for development and production, you should have a copy of the appsettings.json with the modified values and some code like this one:

using System.Diagnostics;
using System.Text.Json;

string settingsText; 
if (Debugger.IsAttached)
{
    settingsText = File.ReadAllText("appsettings.development.json");
}
else
{
    settingsText = File.ReadAllText("appsettings.json");
}
var settings = JsonSerializer.Deserialize<Settings>(settingsText);
Console.WriteLine(settings);

Things start to become complicated. If there was a way to simplify this… In fact, yes there is. .NET provides us with the ConfigurationBuilder class. With it, you can read and merge several files to get the configuration. The following code will merge the appsettings.json and appsettings.development.json into a single class. In production, all you have to do is to remove the appsettings.development.json from the package and only the production file will be used.

To use the ConfigurationBuilder class you must add the NuGet packages Microsoft.Extensions.Configuration and Microsoft.Extensions.Configuration.Binder with

dotnet add package Microsoft.Extensions.Configuration
dotnet add package Microsoft.Extensions.Configuration.Binder

You will also have to add the Microsoft.Extensions.Configuration.Json package to use the AddJsonFile extension method.

One other thing that you will have to do is to tell msbuild to copy the settings file to the output directory. This is done by changing the csproj file, adding this clause

<ItemGroup>
  <Content Include="appsettings*.json">
    <CopyToOutputDirectory>Always</CopyToOutputDirectory>
  </Content>  
</ItemGroup>

Once you do that, this code will read the config files, merge them and print the settings:

IConfigurationRoot config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", false)
    .AddJsonFile("appsettings.development.json", true)
    .Build();
var appdata = config.GetSection(nameof(AppData)).Get<AppData>();
Console.WriteLine(appdata);

The second parameter in the AddJsonFile method tells that the appsettings.development.json file is optional and, if it’s not there, it wont be read. One other advantage is that I don’t need to duplicate all settings. You just need to add the overridden settings in the development file and the other ones will still be available.

Now, one more problem: let’s say we are using an API that requires a client Id and a client Secret. These values are very sensitive and they cannot be distributed. If you are using a public code repository, like GitHub, you cannot add something like this to appsettings.json and push your changes:

{
    "AppData": {
        "Id": "IdOfApp",
        "Name": "NameOfApp",
        "Version": "1.0.0.0"
    },
    "ApiData": {
        "ClientId": "ClientIdOfApp",
        "ClientSecret": "ClientSecretOfApp"
    }
}

That would be a real disaster, because your API codes would be open and you would end up with a massive bill at the end of the month. You could add these keys to appsettings.development.json and add it to the ignored files, so it wouldn’t be uploaded, but there is no guarantee that this won’t happen. Somebody could upload the file and things would be messy again.

The solution, in this case, would be to use the Secret Manager Tool. This tool allows you to store secrets in development mode, in a way that they cannot be shared to other users. This tool doesn’t encrypt any data and must only be used for development purposes. If you want to store the secrets in a safe encrypted way, you should use something like the Azure Key Vault.

To use it, you should initialize the storage with

dotnet user-secrets init

This will initialize the storage and generate a Guid for it and add it to the csproj file:

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <UserSecretsId>fc572277-3ded-4467-9c46-534a075f905b</UserSecretsId>
  </PropertyGroup>

Then, you need to add the package Microsoft.Extensions.Configuration.UserSecrets:

dotnet add package Microsoft.Extensions.Configuration.UserSecrets

We can now start utilizing the user secrets, by adding the new configuration type:

IConfigurationRoot config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", false)
    .AddJsonFile("appsettings.development.json", true)
    .AddUserSecrets<Settings>()
    .Build();

Then, we can add the secret data:

dotnet user-secrets set "ApiData:ClientId" "ClientIdOfApp"
dotnet user-secrets set "ApiData:ClientSecret" "ClientSecretOfApp"

As you can see, the data is flattened in order to be added to the user secrets. You can take a look at it by opening an Explorer window and going to %APPDATA%\Microsoft\UserSecrets\{guid}\secrets.json:

{
  "ApiData:ClientId": "ClientIdOfApp",
  "ApiData:ClientSecret": "ClientSecretOfApp"
}

As you can see, there isn’t any secret here, it’s just a way to store data with no possibility to share it in an open repository.

You can get the values stored with

dotnet user-secrets list

To remove some key from the store, you can use something like

dotnet user-secrets remove ClientId

And to clear all data, you can use

dotnet user-secrets clear

If you have some array data to store, you will have to flatten in the same way, using the index of the element as a part of the name. For example, if you have something like

public class Settings
{
    public AppData AppData { get; set; }
    public ApiData ApiData { get; set; }
    public string[] AllowedHosts { get; set; }
}

You can store the AllowedHosts data with

dotnet user-secrets set "AllowedHosts:0" "microsoft.com"
dotnet user-secrets set "AllowedHosts:1" "google.com"
dotnet user-secrets set "AllowedHosts:2" "amazon.com"

And you can read the settings with some code like this:

IConfigurationRoot config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", false)
    .AddJsonFile("appsettings.development.json", true)
    .AddUserSecrets<Settings>()
    .Build();
var settings = config.Get<Settings>();
foreach (var item in settings.AllowedHosts)
{
    Console.WriteLine(item);
}
Console.WriteLine(settings.AppData);
Console.WriteLine(settings.ApiData);

As you can see, if you need something to keep your development data safe from uploading to a public repository, you can use the user secrets in the same way you would do by using a json file. This simplifies a lot the storage of config files and allows every developer to have their own settings.

The full source code for this project is at https://github.com/bsonnino/SecretStorage

Introduction

I am a long time user of Gmail, and I usually don’t delete any email, I just archive the emails after reading and processing them, to keep my inbox clean.

Last week, I got a notice from Gmail that I was reaching the 15GB free limit and, in order to continue receiving emails, I should I should either buy some extra storage or clean my mail archive.

I know that I store a lot of garbage there, so I decided to clean the archive: the first step was to delete some old newsletters, and some junk email, but this didn’t even scratch the size of my mailbox (maybe it removed about 200Mb of data).

Then I started to use Gmail’s size filters: if you enter “larger:10M” in the query box, Gmail will show only messages with 10Mb or more. This is a great improvement, but there are two gotchas here: the messages aren’t sorted by size and you don’t know the size of every message.

That way, you won’t be able to effectively clean your mailbox – it will be a very difficult task to search among your 1500 messages which ones are good candidates to delete. So I decided to bite the bullet and create a C# program to scan my mailbox, list the largest ones and delete some of them. I decided to create a Universal Windows Platform app, so I could use on both my desktop and Windows Phone with no changes in the app code.

Registering the app with Google

The first step to create the app is to register it with Google, so you can get an app id to use in your app. Go to https://console.developers.google.com/flows/enableapi?apiid=gmail and create a new project. Once you have registered, you must get the credentials, to use in the app.

Figure 1 – Adding credentials

This will create new credentials for your app, which you must download and add to your project. When you download the credentials, you get a file named client_id.json , which you will include in your project. The next step is to create the project.

Creating the project

You must go to Visual Studio and create a new UWP app (blank app).

Figure 2 – Creating a new UWP app

A dialog appears asking you the target version for the app, and you can click OK. Then, you must add the NuGet package Google.Apis.Gmail.v1. This can be done in two ways: in the Solution Explorer, right click in the “References” node and select “Manage NuGet Packages” and search for Gmail, adding the Gmail package.

The second way is to open the Package Manager Console Window and adding the command:

Install-Package Google.Apis.Gmail.v1

Once you have installed the package, you must add the json file with the credentials to your project. Right click the project and select Add/Existing item and add the client_id.json file. Go to the properties window and select Build Action to Content and Copy to Output Directory as Copy always.

Getting User authorization

The first thing you must do in the program is to get the user authorization to access the email. This is done using OAuth2, with this code:

public async Task<UserCredential> GetCredential()
{
    var scopes = new[] { GmailService.Scope.GmailModify };
    var uri = new Uri("ms-appx:///client_id.json");
    _credential = await GoogleWebAuthorizationBroker.AuthorizeAsync(
        uri, scopes, "user", CancellationToken.None);
    return _credential;
}

We call the AuthorizeAsync method of GoogleWebAuthorizationBroker, passing the uri for client_id.json , andthe scope we want (modify emails). You can call this method in the constructor of MainPage:

public MainPage()
{
    this.InitializeComponent();
    GetCredential();
}

When you run the program, it will open a web page to get the user’s authorization. This procedure doesn’t store any password in the application, thus making it safe for the users: they can give their credentials to the Google broker and the broker will send just an authorization token to access the email.

Figure 3 – Web page for authorization

Figure 4 – Authorization consent

If you take a look at Figure 4, you will see that we are not asking for permission to delete the mail. We won’t need this permission, because we are going to just move the messages to trash. Then, the user will be able to review the messages and delete them permanently.

Getting emails

Once you have the authorization, you can get the emails from the server. In MainPage.xaml, add the UI for the application:

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto"/>
        <RowDefinition Height="*"/>
        <RowDefinition Height="Auto"/>
    </Grid.RowDefinitions>
    <Button Content ="Get Messages" Click="GetMessagesClick" 
            HorizontalAlignment="Right" Margin="5" Width="120"/>
    <ListView Grid.Row="1" x:Name="MessagesList" />
    <TextBlock Grid.Row="2" x:Name="CountText" Margin="5"/>
</Grid>

We will have a button to get the messages and add them to the listview. At the bottom, a textblock will display the message count. The code for retrieving the messages is:

private async void GetMessagesClick(object sender, RoutedEventArgs e)
{
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = _credential,
        ApplicationName = AppName,
    });
    UsersResource.MessagesResource.ListRequest request =
    service.Users.Messages.List("me");
    request.Q = "larger:5M";
    request.MaxResults = 1000;
    messages = request.Execute().Messages;
    MessagesList.ItemsSource = messages;
    CountText.Text = $"{messages.Count} messages";
}

We create a request for getting the messages larger than 5Mb and returning a maximum of 1000 results. If you have more than 1000 emails larger than 5Mb, there’s no guarantee you will get the largest emails, but you can change the query settings to satisfy your needs. Then, we query the server and fill the listview. If you run the app and click the button, you will see something like in Figure 5:

Figure 5 – Mail results displayed in the app window

We see only the item type because we didn’t set up an item template. That can be done in MainPage.xaml:

<ListView Grid.Row="1" x:Name="MessagesList" >
    <ListView.ItemTemplate>
        <DataTemplate>
            <StackPanel Margin="5">
                <TextBlock Text="{Binding Id}"/>
                <TextBlock Text="{Binding Snippet}" FontWeight="Bold"/>
                <TextBlock Text="{Binding SizeEstimate}"/>
            </StackPanel>
        </DataTemplate>
    </ListView.ItemTemplate>
</ListView>

Running the app, you will see that the list only shows the Ids for the messages. This first call doesn’t return the full message. To get the message contents, we must do a second call, to retrieve the message contents:

private async void GetMessagesClick(object sender, RoutedEventArgs e)
{
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = _credential,
        ApplicationName = AppName,
    });
    
        UsersResource.MessagesResource.ListRequest request =
        service.Users.Messages.List("me");
        request.Q = "larger:5M";
        request.MaxResults = 1000;
        messages = request.Execute().Messages;
        var sizeEstimate = 0L;
        for (int index = 0; index < messages.Count; index++)
        {
            var message = messages[index];
            var getRequest = service.Users.Messages.Get("me", message.Id);
            getRequest.Format =
                UsersResource.MessagesResource.GetRequest.FormatEnum.Metadata;
            getRequest.MetadataHeaders = new Repeatable<string>(
                new[] { "Subject", "Date", "From" });
            messages[index] = getRequest.Execute();
            sizeEstimate += messages[index].SizeEstimate ?? 0;
        }
    });
    MessagesList.ItemsSource = messages.OrderByDescending(m => m.SizeEstimate));
    CountText.Text = $"{messages.Count} messages. Estimated size: {sizeEstimate:n0}";
}

When we are getting the messages, we limit the data recovered. The default behavior for the Get request is to retrieve the full message, but this would be an overkill. We only get the Subject, Date and From headers for the message. If you run the app, you will get the snippet and the size, but you will see that the app hangs while retrieving the messages. This is not a good thing to do. We must get the messages in the background:

private async void GetMessagesClick(object sender, RoutedEventArgs e)
{
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = _credential,
        ApplicationName = AppName,
    });
    var sizeEstimate = 0L;
    IList<Message> messages = null;
    
    await Task.Run(async () =>
    {
        UsersResource.MessagesResource.ListRequest request =
        service.Users.Messages.List("me");
        request.Q = "larger:5M";
        request.MaxResults = 1000;
        messages = request.Execute().Messages;
        
        for (int index = 0; index < messages.Count; index++)
        {
            var message = messages[index];
            var getRequest = service.Users.Messages.Get("me", message.Id);
            getRequest.Format =
                UsersResource.MessagesResource.GetRequest.FormatEnum.Metadata;
            getRequest.MetadataHeaders = new Repeatable<string>(
                new[] { "Subject", "Date", "From" });
            messages[index] = getRequest.Execute();
            sizeEstimate += messages[index].SizeEstimate ?? 0;
        }
    });
    MessagesList.ItemsSource = messages.OrderByDescending(m => m.SizeEstimate));
    CountText.Text = $"{messages.Count} messages. Estimated size: {sizeEstimate:n0}";
}

Now the code doesn’t block the UI, but there’s no indication of what’s happening. Let’s add a progress bar to the UI:

<TextBlock Grid.Row="2" x:Name="CountText" Margin="5"/>
<Border x:Name="BusyBorder" Grid.Row="0" Grid.RowSpan="3" 
        Background="#40000000" Visibility="Collapsed">
    <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
        <TextBlock Text="downloading messages" x:Name="OperationText"/>
        <ProgressBar x:Name="ProgressBar" Margin="0,5"/>
        <TextBlock x:Name="DownloadText"  HorizontalAlignment="Center"/>
    </StackPanel>
</Border>

To update the progress bar while downloading, we use this code:

private async void GetMessagesClick(object sender, RoutedEventArgs e)
{
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = _credential,
        ApplicationName = AppName,
    });
    var sizeEstimate = 0L;
    IList<Message> messages = null;
    
    BusyBorder.Visibility = Visibility.Visible;
    await Task.Run(async () =>
    {
        UsersResource.MessagesResource.ListRequest request =
        service.Users.Messages.List("me");
        request.Q = "larger:5M";
        request.MaxResults = 1000;
        messages = request.Execute().Messages;
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            ProgressBar.Maximum = messages.Count);

        for (int index = 0; index < messages.Count; index++)
        {
            var message = messages[index];
            var getRequest = service.Users.Messages.Get("me", message.Id);
            getRequest.Format =
                UsersResource.MessagesResource.GetRequest.FormatEnum.Metadata;
            getRequest.MetadataHeaders = new Repeatable<string>(
                new[] { "Subject", "Date", "From" });
            messages[index] = getRequest.Execute();
            sizeEstimate += messages[index].SizeEstimate ?? 0;
            
            var index1 = index+1;
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                ProgressBar.Value = index1;
                DownloadText.Text = $"{index1} of {messages.Count}";
            });
        }
    });
    BusyBorder.Visibility = Visibility.Collapsed;
    MessagesList.ItemsSource = messages.OrderByDescending(m => m.SizeEstimate));
    CountText.Text = $"{messages.Count} messages. Estimated size: {sizeEstimate:n0}";
}

We set the visibility of the Busy border to visible before downloading the messages. As we download the messages, we update the progress bar. We are running the code in a background thread, so we can’t update the progress bar and the text directly, we must use the Dispatcher to update the controls in the main thread. Now, when we run the code, the busy border is shown and the progress bar is updated with the download count. At the end, we get the messages, with the snippets and size.

Figure 6 – Message results

You can see some problems in this display:

  • It’s far from good, and should be improved
  • It doesn’t show the subject, date and who sent the message
  • The snippet format is encoded and should be decoded
  • The size could be formatted

We can fix these issues by creating a new class:

public class EmailMessage
{
    public string Id { get; set; }
    public bool IsSelected { get; set; }
    public string Snippet { get; set; }
    public string SizeEstimate { get; set; }
    public string From { get; set; }
    public string Date { get; set; }
    public string Subject { get; set; }
}

And use it, instead of the Message class:

private async void GetMessagesClick(object sender, RoutedEventArgs e)
{
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = _credential,
        ApplicationName = AppName,
    });
    var sizeEstimate = 0L;
    IList<Message> messages = null;
    var emailMessages = new List<EmailMessage>();
    OperationText.Text = "downloading messages";
    BusyBorder.Visibility = Visibility.Visible;
    await Task.Run(async () =>
    {
        UsersResource.MessagesResource.ListRequest request =
        service.Users.Messages.List("me");
        request.Q = "larger:5M";
        request.MaxResults = 1000;
        messages = request.Execute().Messages;
        await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            ProgressBar.Maximum = messages.Count);

        for (int index = 0; index < messages.Count; index++)
        {
            var message = messages[index];
            var getRequest = service.Users.Messages.Get("me", message.Id);
            getRequest.Format =
                UsersResource.MessagesResource.GetRequest.FormatEnum.Metadata;
            getRequest.MetadataHeaders = new Repeatable<string>(
                new[] { "Subject", "Date", "From" });
            messages[index] = getRequest.Execute();
            sizeEstimate += messages[index].SizeEstimate ?? 0;
            emailMessages.Add(new EmailMessage()
            {
                Id = messages[index].Id,
                Snippet = WebUtility.HtmlDecode(messages[index].Snippet),
                SizeEstimate = $"{messages[index].SizeEstimate:n0}",
                From = messages[index].Payload.Headers.FirstOrDefault(h => 
                    h.Name == "From").Value,
                Subject = messages[index].Payload.Headers.FirstOrDefault(h => 
                    h.Name == "Subject").Value,
                Date = messages[index].Payload.Headers.FirstOrDefault(h => 
                    h.Name == "Date").Value,
            });
            var index1 = index+1;
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                ProgressBar.Value = index1;
                DownloadText.Text = $"{index1} of {messages.Count}";
            });
        }
    });
    BusyBorder.Visibility = Visibility.Collapsed;
    MessagesList.ItemsSource = new ObservableCollection<EmailMessage>(
        emailMessages.OrderByDescending(m => m.SizeEstimate));
    CountText.Text = $"{messages.Count} messages. Estimated size: {sizeEstimate:n0}";
}

With this new code, we can change the item template, to show the new data:

<ListView.ItemTemplate>
    <DataTemplate>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="40"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <CheckBox HorizontalAlignment="Left" VerticalAlignment="Center" 
                      IsChecked="{Binding IsSelected, Mode=TwoWay}" Margin="5"/>
            <StackPanel Grid.Column="1" Margin="5">
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="From:" Margin="0,0,5,0"/>
                    <TextBlock Text="{Binding From}"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Date:" Margin="0,0,5,0"/>
                    <TextBlock Text="{Binding Date}"/>
                </StackPanel>
                <StackPanel Orientation="Horizontal">
                    <TextBlock Text="Size: " Margin="0,0,5,0"/>
                    <TextBlock Text="{Binding SizeEstimate}"/>
                </StackPanel>
                <TextBlock Text="{Binding Subject}"/>
                <TextBlock Text="{Binding Snippet}" FontWeight="Bold"/>
            </StackPanel>
            <Rectangle Grid.Column="0" Grid.ColumnSpan="2" 
                       HorizontalAlignment="Stretch" VerticalAlignment="Bottom" 
                       Height="1" Fill="Black"/>
        </Grid>
    </DataTemplate>
</ListView.ItemTemplate>

With this code, we get a result like this:

Figure 7 – Message results with more data

We could still improve the performance of the app by making multiple requests for messages at the same time, but I leave this for you.

Deleting emails from the server

Now, the only task that remains is to delete the emails from the server. For that, we must add another button:

<Button Content ="Get Messages" Click="GetMessagesClick" 
        HorizontalAlignment="Right" Margin="5" Width="120"/>
<Button Grid.Row="0" Content ="Delete Messages" Click="DeleteMessagesClick" 
        HorizontalAlignment="Right" Margin="5,5,140,5" Width="120"/>

The code for deleting messages is this:

private async void DeleteMessagesClick(object sender, RoutedEventArgs e)
{
    var messages = (ObservableCollection<EmailMessage>) MessagesList.ItemsSource;
    var messagesToDelete = messages.Where(m => m.IsSelected).ToList();
    if (!messagesToDelete.Any())
    {
        await (new MessageDialog("There are no selected messages to delete")).ShowAsync();
        return;
    }
    var service = new GmailService(new BaseClientService.Initializer()
    {
        HttpClientInitializer = _credential,
        ApplicationName = AppName,
    });
    OperationText.Text = "deleting messages";
    ProgressBar.Maximum = messagesToDelete.Count;
    DownloadText.Text = "";
    BusyBorder.Visibility = Visibility.Visible;
    var sizeEstimate = messages.Sum(m => Convert.ToInt64(m.SizeEstimate));
    await Task.Run(async () =>
    {
        for (int index = 0; index < messagesToDelete.Count; index++)
        {
            var message = messagesToDelete[index];
            var response = service.Users.Messages.Trash("me", message.Id);
            response.Execute();
            var index1 = index+1;
            await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
            {
                ProgressBar.Value = index1;
                DownloadText.Text = $"{index1} of {messagesToDelete.Count}";
                messages.Remove(message);
                sizeEstimate -= Convert.ToInt64(message.SizeEstimate);
                CountText.Text = $"{messages.Count} messages. Estimated size: {sizeEstimate:n0}";
            });
        }
    });
    BusyBorder.Visibility = Visibility.Collapsed;
}

If you run the app, you can select the messages you want and delete them. They will be moved to the trash folder, so you can double check the messages before deleting them.

Conclusion

This article has shown how to access the Gmail messages using the Gmail API and delete the largest messages, all in your Windows 10 app. Oh, and did I mention that the same app works also in Windows Phone, or in other Windows 10 devices?

All the source code for this article is available on GitHub, at http://github.com/bsonnino/LargeEmailsGmail

This article was first published at https://learn.microsoft.com/en-us/archive/blogs/mvpawardprogram/accessing-and-deleting-large-e-mails-in-gmail-with-c

Sometimes, we need to get our display disposition to position windows on them in specific places. The usual way to do it in .NET is to use the Screen class, with a code like this one:

internal record Rect(int X, int Y, int Width, int Height);
internal record Display(string DeviceName, Rect Bounds, Rect WorkingArea, double ScalingFactor);

private void InitializeDisplayCanvas()
{
    
    var minX = 0;
    var minY = 0;
    var maxX = 0;
    var maxY = 0;
    foreach(var screen in Screen.AllScreens)
    {
        if (minX > screen.WorkingArea.X)
            minX = screen.WorkingArea.X;
        if (minY > screen.WorkingArea.Y)
            minY = screen.WorkingArea.Y;
        if (maxX < screen.WorkingArea.X+screen.WorkingArea.Width)
            maxX = screen.WorkingArea.X+screen.WorkingArea.Width;
        if (maxY < screen.WorkingArea.Y+screen.WorkingArea.Height)
            maxY = screen.WorkingArea.Y+screen.WorkingArea.Height;

        _displays.Add(new Display(screen.DeviceName, screen.Bounds, screen.WorkingArea, 1.0));
    }
    DisplayCanvas.Width = maxX - minX;
    DisplayCanvas.Height = maxY - minY;
    DisplayCanvas.RenderTransform = new TranslateTransform(-minX, -minY);
    var background = new System.Windows.Shapes.Rectangle
    {
        Width = DisplayCanvas.Width,
        Height = DisplayCanvas.Height,
        Fill = new SolidColorBrush(System.Windows.Media.Color.FromArgb(1,242,242,242)),
    };
    Canvas.SetLeft(background, minX);
    Canvas.SetTop(background, minY);
    DisplayCanvas.Children.Add(background);
    var numDisplay = 0;
    foreach (var display in _displays)
    {
        numDisplay++;
        var border = new Border
        {
            Width = display.WorkingArea.Width,
            Height = display.WorkingArea.Height,
            Background = System.Windows.Media.Brushes.DarkGray,
            CornerRadius = new CornerRadius(30)
        };
        var text = new TextBlock
        {
            Text = numDisplay.ToString(),
            FontSize = 200,
            FontWeight = FontWeights.Bold,  
            HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
        };
        border.Child = text;
        Canvas.SetLeft(border, display.WorkingArea.Left);
        Canvas.SetTop(border, display.WorkingArea.Top);
        DisplayCanvas.Children.Add(border);
    }

}

If you run the code, you will see something like this:

The monitors aren’t contiguous, as you would expect. But, the worst is that it doesn’t work well. If you try to position a window in the center of the middle monitor, with a code like this:

private void Button_Click(object sender, RoutedEventArgs e)
{
    var display = _displays[0];
    var window = new NewWindow
    {
        Top = display.WorkingArea.Top + (display.WorkingArea.Height - 200) / 2,
        Left = display.WorkingArea.Left + (display.WorkingArea.Width - 200) / 2,
    };
    window.Show();
}

You will get something like this:

As you can see, the window is far from centered. And why is that? The reason for these problems are the usage of high DPI. When you set the displays in you system, you set the resolution and the scaling factor:

In my setup, I have three monitors:

  • 1920×1080 125%
  • 3840×2160 150%
  • 1920×1080 100%

This scale factor is not taken in account when you are enumerating the displays and, when I am enumerating them, I have no way of getting this value. That way, everything is positioned in the wrong place. It would work fine if all monitors had a scaling factor of 100%, but most of the time that’s not true.

Researching for High DPI WPF, I came to this page, which shows the use of the appmanifest, so I gave it a try. I added a new item, Application Manifest and uncommented these lines:

<application xmlns="urn:schemas-microsoft-com:asm.v3">
    <windowsSettings>
      <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
      <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
    </windowsSettings>
  </application>

And nothing happened. Then I added this line:

<windowsSettings>
  <dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitor</dpiAwareness>
  <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>

Running the project, I got the correct display settings:

But the secondary screen is positioned in the wrong place. If I change the dpiAwareness clause to Unaware, I get the wrong display disposition, but the window is positioned at the center!

We need to get the scale factor for each monitor, to get the correct values in both cases.

Before going further we must notice that this code has also another problem: it’s a WPF app that is using a Winforms class: Screen is declared in System.Windows.Forms and there is no equivalent in WPF. To use, it you must add UseWindowsForms in the csproj:

<Project Sdk="Microsoft.NET.Sdk">
	<PropertyGroup>
		<OutputType>WinExe</OutputType>
		<TargetFramework>net6.0-windows</TargetFramework>
		<Nullable>enable</Nullable>
		<UseWPF>true</UseWPF>
		<UseWindowsForms>true</UseWindowsForms>
	</PropertyGroup>
</Project>

That is something I really don’t like to do: use Winforms in a WPF project. If you want to check the project, the branch I’ve used is here.

So, I tried to find a way to enumerate the displays in WPF and have the right scaling factor, and I found two ways: query the registry or use Win32 API. Yes, the API that is available since the beginning of Windows, and it’s still there.

We could go to http://pinvoke.net/ to get the signatures we need for our project. This is a great site and a nice resource when you want to use Win32 APIs in C#, but we’ll use another resource: CsWin32, a nice source generator that generates the P/Invokes for us. I have already written an article about it, if you didn’t see it, you should check it out.

For that, we should install the NuGet package Microsoft.Windows.CsWin32 (be sure to check the pre-release box). Once installed, you must create a text file and name it NativeMethods.txt. There, we will add the names of all the methods and structures we need.

The first function we need is EnumDisplayMonitors, which we add there. With that, we can use it in our method:

private unsafe void InitializeDisplayCanvas()
{
    Windows.Win32.PInvoke.EnumDisplayMonitors(null, null, enumProc, IntPtr.Zero);
}

We are passing all parameters as null, except for the third one, which is the callback function. As I don’t know the parameters of this function, I will let Visual Studio create it for me. Just press Ctrl+. and Generate method enumProc and Visual Studio will generate the method for us:

private unsafe BOOL enumProc(HMONITOR param0, HDC param1, RECT* param2, LPARAM param3)
{
    throw new NotImplementedException();
}

We can change the names of the parameters and add the return value true to continue the enumeration. This function will be called for each monitor in the system and will pass in the first parameter the handle of the monitor. With that handle we can determine its properties with GetMonitorInfo (which we add in NativeMethods.txt) and use it to get the monitor information. This function uses a MONITORINFO struct as a parameter, and we must declare it before calling the function:

private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
{
    var mi = new MONITORINFO
    {
        cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFO))
    };
    if (Windows.Win32.PInvoke.GetMonitorInfo(monitor, ref mi))
    {

    }
    return true;
}

You have noticed that we’ve set the size of the structure before passing it to the function. This is common to many WinApi functions and forgetting to set this member or setting it with a wrong value is a common source of bugs.

MONITORINFO is declared in Windows.Win32.Graphics.Gdi, which is included in the usings for the file. Now, mi has the data for the monitor:

internal partial struct MONITORINFO
{
	internal uint cbSize;
	internal winmdroot.Foundation.RECT rcMonitor;
	internal winmdroot.Foundation.RECT rcWork;
	internal uint dwFlags;
}

But it doesn’t have the name of the monitor. This was solved by using the structure MONITORINFOEX, which has the name of the monitor. There is a catch, here: although GetMonitorInfo has an overload that uses the MONITORINFOEX structure, it’s not declared in CsWin32, so we must do a trick, here:

private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
{
    var mi = new MONITORINFOEXW();
    mi.monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFOEXW));
    
    if (Windows.Win32.PInvoke.GetMonitorInfo(monitor, (MONITORINFO*) &mi))
    {

    }
    return true;
}

MONITORINFOEX is not declared automatically, you must use the explicit wide version, MONITORINFOEXW and add the impport to NativeMethods.txt. To use it, you must create the structure, initialize it and then cast the pointer to a pointer of MONITORINFO. It’s not the most beautiful code, but it works. Now we have the code to enumerate the displays:

public class DisplayList
{
    private List<Display> _displays = new List<Display>();

    public DisplayList()
    {
        QueryDisplayDevices();
    }

    public List<Display> Displays => _displays;

    private unsafe void QueryDisplayDevices()
    {
        PInvoke.EnumDisplayMonitors(null, null, enumProc, IntPtr.Zero);
    }

    private unsafe BOOL enumProc(HMONITOR monitor, HDC hdc, RECT* clipRect, LPARAM data)
    {
        var mi = new MONITORINFOEXW();
        mi.monitorInfo.cbSize = (uint)Marshal.SizeOf(typeof(MONITORINFOEXW));

        if (PInvoke.GetMonitorInfo(monitor, (MONITORINFO*)&mi))
        {
            var display = new Display(mi.szDevice.ToString(),
                new Rect(mi.monitorInfo.rcMonitor.left, mi.monitorInfo.rcMonitor.top, 
                    mi.monitorInfo.rcMonitor.Width, mi.monitorInfo.rcMonitor.Height),
                new Rect(mi.monitorInfo.rcWork.left, mi.monitorInfo.rcWork.top, 
                    mi.monitorInfo.rcWork.Width, mi.monitorInfo.rcWork.Height),
                1);
            _displays.Add(display);
        }
        return true;
    }
}

private void InitializeDisplayCanvas()
{
    var displayList = new DisplayList();
    _displays = displayList.Displays;
    InitializeCanvasWithDisplays();
}

private void InitializeCanvasWithDisplays()
{
    var minX = 0;
    var minY = 0;
    var maxX = 0;
    var maxY = 0;
    foreach (var display in _displays)
    {
        if (minX > display.WorkingArea.X)
            minX = display.WorkingArea.X;
        if (minY > display.WorkingArea.Y)
            minY = display.WorkingArea.Y;
        if (maxX < display.WorkingArea.X + display.WorkingArea.Width)
            maxX = display.WorkingArea.X + display.WorkingArea.Width;
        if (maxY < display.WorkingArea.Y + display.WorkingArea.Height)
            maxY = display.WorkingArea.Y + display.WorkingArea.Height;
    }
    DisplayCanvas.Width = maxX - minX;
    DisplayCanvas.Height = maxY - minY;
    DisplayCanvas.RenderTransform = new TranslateTransform(-minX, -minY);
    var background = new System.Windows.Shapes.Rectangle
    {
        Width = DisplayCanvas.Width,
        Height = DisplayCanvas.Height,
        Fill = new SolidColorBrush(System.Windows.Media.Color.FromArgb(1, 242, 242, 242)),
    };
    Canvas.SetLeft(background, minX);
    Canvas.SetTop(background, minY);
    DisplayCanvas.Children.Add(background);
    var numDisplay = 0;
    foreach (var display in _displays)
    {
        numDisplay++;
        var border = new Border
        {
            Width = display.WorkingArea.Width,
            Height = display.WorkingArea.Height,
            Background = System.Windows.Media.Brushes.DarkGray,
            CornerRadius = new CornerRadius(30)
        };
        var text = new TextBlock
        {
            Text = numDisplay.ToString(),
            FontSize = 200,
            FontWeight = FontWeights.Bold,
            HorizontalAlignment = System.Windows.HorizontalAlignment.Center,
            VerticalAlignment = VerticalAlignment.Center,
        };
        border.Child = text;
        Canvas.SetLeft(border, display.WorkingArea.X);
        Canvas.SetTop(border, display.WorkingArea.Y);
        DisplayCanvas.Children.Add(border);
    }
}
private void Button_Click(object sender, RoutedEventArgs e)
{
    var display = _displays[0];
    var window = new NewWindow
    {
        Top = display.Bounds.Y + (display.Bounds.Height - 200) / display.ScalingFactor / 2,
        Left = display.Bounds.X + (display.Bounds.Width - 200) / display.ScalingFactor / 2,
    };
    window.Show();
}

If you run this code, you’ll get the same result we’ve got with the previous version, but at least we don’t have to include WinForms here. But we can go a step further and get the scale for the monitors. For that, we must use the EnumDisplaySettings function, like this:

if (PInvoke.GetMonitorInfo(monitor, (MONITORINFO*)&mi))
{
    var dm = new DEVMODEW
    {
        dmSize = (ushort)Marshal.SizeOf(typeof(DEVMODEW))
    };
    PInvoke.EnumDisplaySettings(mi.szDevice.ToString(), ENUM_DISPLAY_SETTINGS_MODE.ENUM_CURRENT_SETTINGS, ref dm);

    var scalingFactor = Math.Round((double)dm.dmPelsWidth / mi.monitorInfo.rcMonitor.Width, 2);
    var display = new Display(mi.szDevice.ToString(),
        new Rect(mi.monitorInfo.rcMonitor.left, mi.monitorInfo.rcMonitor.top, 
            (int)(mi.monitorInfo.rcMonitor.Width * scalingFactor), (int)(mi.monitorInfo.rcMonitor.Height * scalingFactor)),
        new Rect(mi.monitorInfo.rcWork.left, mi.monitorInfo.rcWork.top, 
            (int)(mi.monitorInfo.rcWork.Width * scalingFactor), (int)(mi.monitorInfo.rcWork.Height * scalingFactor)),
        scalingFactor);
    _displays.Add(display);
}

Now, if you run the code, you’ll see that it runs fine and positions the window in the center of the display.

We now have a WPF application that can detect all installed displays and position a window correctly in any display. The key to that is, besides using the Win32 API to get the display information, to use the App Manifest dpiAwareness setting to Unaware. If you change to any other setting, you will get the wrong positions, because the scaling factors will only be correct when the Unaware setting is used.

The full source code for this article is at https://github.com/bsonnino/EnumeratingDisplays

When you are using the MVVM pattern, at some time, you have to send data between ViewModels. For example, a detail ViewModel must tell the collection ViewModel that the current item should be deleted, or when you have a main view that opens details views and keeps track of them and must be acknowledged of any of them closing.
This is a very common pattern and usually is solved by coupling the two ViewModels.
That’s a solution, but not the best one, because coupling makes difficult maintenance and testing. For example, we’ll use the WPF project at https://github.com/bsonnino/MessengerToolkit/tree/Original.

This is a WPF project with a main window that shows a grid with all the customers:

When you click the button at the right, it opens a secondary window with the details:

This project is composed by two ViewModels, MainViewModel and CustomerViewModel. MainViewModel serves the main view and performs all actions for the buttons: Filter the data, Add a new customer and Save the data:

public partial class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;
    private readonly INavigationService _navigationService;
    
    public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
    {
        _customerRepository = customerRepository ?? 
                              throw new ArgumentNullException("customerRepository");
        Customers = new ObservableCollection<CustomerViewModel>(
            _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
        _navigationService = navigationService;
    }

    [ObservableProperty]
    private ObservableCollection<CustomerViewModel> _customers;

    [ObservableProperty]
    private int _windowCount;

    [RelayCommand]
    private void Add()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        var vm = new CustomerViewModel(customer);
        Customers.Add(vm);
        _navigationService.Navigate(vm);
    }

    [RelayCommand]
    private void Save()
    {
        _customerRepository.Commit();
    }

    [RelayCommand]
    private void Search(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((CustomerViewModel)c).Country?.ToLower().Contains(textToSearch.ToLower()) == true;
        else
            coll.Filter = null;
    }

    [RelayCommand]
    private void ShowDetails(CustomerViewModel vm)
    {
        _navigationService.Navigate(vm);
        WindowCount++;
    }
}

It uses the features available in version 8 of the MVVM toolkit. In this code, we can see two things:

  • The ViewModel maintains the open windows count. When you click on the button to show the details, it will increase the window count. The problem here is to decrease the count once the window is closed. Right now, the count only increases 😃
  • To open the detail window, we could use something like:
private void ShowDetails(CustomerViewModel vm)
{
    var detailWindow = new Detail { DataContext = vm };
    detailWindow.Show();
    WindowCount++;
}

This approach has two flaws:

  • We are coupling the ViewModel to the Detail view
  • We are calling View details in the ViewModel

This makes this code to be completely untestable and defeats all purpose of the MVVM pattern. So, we must find another solution. What I devised is a NavigationService, that will open a Window, depending on the type of the ViewModel that is passed to the Navigate method. This service is very simple, and has only one method,Navigate:

public interface INavigationService
{
    void Navigate(object arg);
}

public class NavigationService : INavigationService
{
    private readonly Dictionary<Type, Type> viewMapping = new()
    {
        [typeof(MainViewModel)] = typeof(MainWindow),
        [typeof(CustomerViewModel)] = typeof(Detail),
    };

    public void Navigate(object arg)
    {
        Type vmType = arg.GetType();
        if (viewMapping.ContainsKey(vmType))
        {
            var windowType = viewMapping[vmType];
            var window = (System.Windows.Window)Activator.CreateInstance(windowType);
            window.DataContext = arg;
            window.Show();
        }
    }
}

We’ve defined an interface, INavigationService, and created a class that implements it, NavigationService. This class has a dictionary with all the ViewModel types that have a corresponding window type, with their corresponding window type as the value of the item.

When we call Navigate and pass the instance of the ViewModel, the method will check its type, verify if it exists in the dictionary and instantiate the window, setting the VM as its DataContext and opening the Window. That way, we decouple the ViewModel from the views. The navigation service is injected in the constructor of MainViewModel by the use of dependency injection.

In App.xaml.cs we are registering the services and the VM. That way, we get the right data injected to the main VM:

public IServiceProvider Services { get; }

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<INavigationService, NavigationService>();
    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<MainViewModel>();

    return services.BuildServiceProvider();
}

One note is ShowDetails, which in fact is a command that should be sent to the CustomerViewModel, because it’s tied to every CustomerViewModel in the grid. What we did is to tie it to the ViewModel of the main view with:

<DataGridTemplateColumn>
    <DataGridTemplateColumn.CellTemplate>
        <DataTemplate>
            <Button Command="{Binding DataContext.ShowDetailsCommand, 
                RelativeSource={RelativeSource AncestorType=Window}}" 
                    CommandParameter="{Binding}" ToolTip="Details">
                <TextBlock Text="" FontFamily="Segoe UI Symbol" />
            </Button>
        </DataTemplate>
    </DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>

We set the command to ShowDetailsCommand in the datacontext of the parent window, and pass the current CustomerViewModel as a parameter. That suffices to do the trick.

Now, onto our problems:

  • The Window Closing event is just that – an event, not a command and there is no direct way to handle it in the ViewModel
  • Even if we can handle it in the ViewModel, there is no direct way to warn the main viewmodel about the change
  • When we click the Remove button in the Detail view, we cannot remove the item from the list, because the list is on the Main ViewModel, which is not accessible from the Customer ViewModel

For the first problem, we could handle the Closing event in the code behind of the Detail view:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    var vm = DataContext as CustomerViewModel;
    vm.ClosingCommand.Execute(null);
}

or, better, send the closing signal directly to the main viewmodel:

private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
    var vm = App.Current.MainVM;
    vm.ClosingCommand.Execute(null);
}

This would work fine, but purists would say that it’s a code smell. So, let’s do it in the MVVM way. For that, we must install the Microsoft.Xaml.Behaviors.WPF NuGet package, that has some behaviors we will need in the detail window:

<Window x:Class="CustomerApp.Detail"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:b="http://schemas.microsoft.com/xaml/behaviors"
        mc:Ignorable="d"
        Height="440"
        Width="520"
        Background="AliceBlue">
    <b:Interaction.Triggers>
        <b:EventTrigger EventName="Closing">
            <b:InvokeCommandAction Command="{Binding ClosingCommand}" />
        </b:EventTrigger>
    </b:Interaction.Triggers>

There, we have Interaction.Triggers, to add the triggers we want. We want to use the EventTrigger, that is triggered whe an event is fired. In our case, it’s the Closing event. When it’s fired, it will invoke the command ClosingCommand in the detail window. Problem solved. No more code behind.

To solve the second and third problem, we would have to couple our Detail ViewModel to the Main ViewModel and have a variable that stores its instance. One way would be to pass the VM as parameter:

private readonly MainViewModel _mainvm;

public CustomerViewModel(Customer customer, MainViewModel mainvm)
{
    _customer = customer;
    _mainvm = mainvm;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

One other way would be to get the App property:

private readonly MainViewModel _mainvm = App.Current.MainVM;

or to use the Service Locator to get the instance:

private readonly MainViewModel _mainvm = (MainViewModel)App.Current.Services.GetService(typeof(MainViewModel));

No matter with way we are using it, we are coupling the two viewmodels and that will make testing difficult. Passing the VM as a parameter for the constructor will be the more testable way, the other two would get us in trouble: we should have an App created for the second option and have an App created and a Service Locator in place to get our instance. But wouldn’t it be better if we could get rid of the instance of Main ViewModel and do not have them coupled?

In fact there is: Messages. Messaging is a mechanism that decouples the sender and receiver of a message, there is not even need to have a receiver for the message, or there may be many receivers from it – the sender doesn’t care.

The messenger is based on the Mediator pattern, where the Messenger class acts as the mediator for the messages. The potential receivers subscribe for receiving some kind of messages and, when some sender sends a message, the messenger will send it to the subscribers that are willing to receive that kind of message.

In the MVVM Toolkit, you can define messengers that implement the IMessenger interface, but the class WeakReferenceMessenger exposes a Default property, which offers a thread safe implementation for this interface, which makes it easier to implement. This class has a Register method, which will register the client for some message type and will pass the callback function that will be called when a message of the desired type is received. If you wat toknow more about that, you can take a look at https://docs.microsoft.com/en-us/windows/communitytoolkit/mvvm/messenger.

For our needs, we have to define two types of different messages: a message for when a customer should be deleted and the other one, when the window is closed:

public class WindowClosedMessage: ValueChangedMessage<CustomerViewModel>
{
    public WindowClosedMessage(CustomerViewModel vm) : base(vm)
    {

    }
}

public class ViewModelDeletedMessage : ValueChangedMessage<CustomerViewModel>
{
    public ViewModelDeletedMessage(CustomerViewModel vm) : base(vm)
    {

    }
}

The messages inherit from ValueChangedMessage. This kind of messages are sent to the recipients and the execution goes on. If, on the other way, the Sender needs an answer from the receiver, it will send a message of the RequestMessage type. In this case, the sender will wait the response that the receiver will send using the Reply method of the message. In our case, both messages don’t need a reply, so we use the ValueChangedMessage type. We must create two different types of message, because we want different processing for each one.

Now, we must send the messages in CustomerViewModel, in the Closing and Delete commands:

[RelayCommand]
private void Delete()
{
    WeakReferenceMessenger.Default.Send(new ViewModelDeletedMessage(this));
}

[RelayCommand]
private void Closing()
{
    WeakReferenceMessenger.Default.Send(new WindowClosedMessage(this));
}

The processing for these messages is done in the MainViewModel class:

public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
    _navigationService = navigationService;
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
    });
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}

private void DeleteCustomer(CustomerViewModel vm)
{
    Customers.Remove(vm);
    var deletedCustomer = _customerRepository.Customers.FirstOrDefault(c => c.CustomerId == vm.CustomerId);
    if (deletedCustomer != null)
    {
        _customerRepository.Remove(deletedCustomer);
    }
}

Now, when you run the app, you will see that when you close the detail window, the window count decreases. We could even keep a list of open windows, with some code like this:

[ObservableProperty]
private ObservableCollection<CustomerViewModel> _openWindows = new ObservableCollection<CustomerViewModel>();

public MainViewModel(ICustomerRepository customerRepository, INavigationService navigationService)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c)));
    _navigationService = navigationService;
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
        _openWindows.Remove(m.Value);
    });
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}

....

[RelayCommand]
private void ShowDetails(CustomerViewModel vm)
{
    _navigationService.Navigate(vm);
    WindowCount++;
    _openWindows.Add(vm);
}

When you click the Delete button in the detail window, the corresponding item is deleted in the main list, but the window remains open, and it should not ne there anymore, as the corresponding customer does not exist anymore. We could use some code behind to close the window, but I’d prefer another approach: change the navigation service to allow closing the windows. For that, we should keep the lists of open windows and have another method, Close, to close the selected window:

public interface INavigationService
{
    void Navigate(object arg);
    void Close(object arg);
}

public class NavigationService : INavigationService
{
    private readonly Dictionary<Type, Type> viewMapping = new()
    {
        [typeof(MainViewModel)] = typeof(MainWindow),
        [typeof(CustomerViewModel)] = typeof(Detail),
    };

    private readonly Dictionary<object, List<System.Windows.Window>> _openWindows = new();

    public void Navigate(object arg)
    {
        Type vmType = arg.GetType();
        if (viewMapping.ContainsKey(vmType))
        {
            var windowType = viewMapping[vmType];
            var window = (System.Windows.Window)Activator.CreateInstance(windowType);
            window.DataContext = arg;
            if (!_openWindows.ContainsKey(arg))
            {
                _openWindows.Add(arg, new List<System.Windows.Window>());
            }
            _openWindows[arg].Add(window);
            window.Show();
        }
    }

    public void Close(object arg)
    {
        if (_openWindows.ContainsKey(arg))
        {
            foreach(var window in _openWindows[arg])
            {
                window.Close();
            }
        }
    }
}

In this case, we keep a list of windows open for each customer. If we try to open another window for the same customer, the corresponding window will be added to the corresponding list. When the close method is called, all windows for that customer will be closed at once. The DeleteCustomer in MainViewModel becomes

private void DeleteCustomer(CustomerViewModel vm)
{
    Customers.Remove(vm);
    var deletedCustomer = _customerRepository.Customers.FirstOrDefault(c => c.CustomerId == vm.CustomerId);
    if (deletedCustomer != null)
    {
        _customerRepository.Remove(deletedCustomer);
    }
    _navigationService.Close(vm);
}

And now everything works fine.

But, what about testing? We did all this to ease testing, let’s test the VMs. In the Test project, let’s create a class CustomerViewModelTests.cs and add some tests:

[TestClass]
public class CustomerViewModelTests
{
    [TestMethod]
    public void Constructor_NullCustomer_ShouldThrow()
    {
        Action act = () => new CustomerViewModel(null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customer"));
    }
}

If we run this test, we’ll see it doesn’t pass. We haven’t made a check for null customers, so we have to change that in the code:

public CustomerViewModel(Customer customer)
{
    if (customer == null)
        throw new ArgumentNullException("customer");
    _customer = customer;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

The test now passes and we can continue. For the next test, I’d like to have a customer filled with data, and FakeItEasy (which we are using for our test mocks) fills the data with nulls and that’s not what we want. In this article I show how to use Bogus to create fake data. We can use AutoBogus.FakeItEasy to integrate it with FakeItEasy. Install the package AutoBogus.FakeItEasy and let’s create the second test

[TestMethod]
public void Constructor_ShouldSetFields()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    customerVM.CustomerId.Should().Be(customer.CustomerId);
    customerVM.CompanyName.Should().Be(customer.CompanyName);
    customerVM.ContactName.Should().Be(customer.ContactName);
    customerVM.ContactTitle.Should().Be(customer.ContactTitle);
    customerVM.Address.Should().Be(customer.Address);
    customerVM.City.Should().Be(customer.City);
    customerVM.Region.Should().Be(customer.Region);
    customerVM.PostalCode.Should().Be(customer.PostalCode);
    customerVM.Country.Should().Be(customer.Country);
    customerVM.Phone.Should().Be(customer.Phone);
    customerVM.Fax.Should().Be(customer.Fax);
}

For the third test, we will be testing the Delete command, and we want to check if if sends the correct message. For that, we must install the _Community.Toolkit.MVVM in the test project. Then, we can create the tests for the two commands:

[TestMethod]
public void DeleteCommand_ShouldSendMessageWithVM()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    object callbackResponse = null;
    var waitEvent = new AutoResetEvent(false);
    WeakReferenceMessenger.Default.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        callbackResponse = customerVM;
        waitEvent.Set();
    });
    customerVM.DeleteCommand.Execute(null);
    waitEvent.WaitOne(100);
    callbackResponse.Should().Be(customerVM);
}

[TestMethod]
public void CloseCommand_ShouldSendMessageWithVM()
{
    var customer = AutoFaker.Generate<Customer>();
    var customerVM = new CustomerViewModel(customer);
    object callbackResponse = null;
    var waitEvent = new AutoResetEvent(false);
    WeakReferenceMessenger.Default.Register<WindowClosedMessage>(this, (r, m) =>
    {
        callbackResponse = customerVM;
        waitEvent.Set();
    });
    customerVM.ClosingCommand.Execute(null);
    waitEvent.WaitOne(100);
    callbackResponse.Should().Be(customerVM);
}

These two tests have a particularity: the result is set in the callback, so we may not have it immediately, so we can have a flaky test if we don’t wait some time before testing the value – sometimes it may pass and sometimes not. For this issue, I’ve used an AutoResetEvent to be set when the callback is called and wait 100ms to see if it’s called. That makes the trick.

For the MainViewModel tests, we can do this:

[TestClass]
public class MainViewModelTests
{
    [TestMethod]
    public void Constructor_NullRepository_ShouldThrow()
    {
        Action act = () => new MainViewModel(null, null);

        act.Should().Throw<ArgumentNullException>()
            .Where(e => e.Message.Contains("customerRepository"));
    }

    [TestMethod]
    public void Constructor_Customers_ShouldHaveValue()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);

        vm.Customers.Count.Should().Be(customers.Count);
    }

    [TestMethod]
    public void AddCommand_ShouldAddInRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        A.CallTo(() => repository.Add(A<Customer>._)).MustHaveHappened();
    }

    [TestMethod]
    public void AddCommand_ShouldAddInCollection()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        vm.Customers.Count.Should().Be(11);
    }

    [TestMethod]
    public void AddCommand_ShouldCallNavigate()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.AddCommand.Execute(null);
        A.CallTo(() => navigationService.Navigate(A<CustomerViewModel>.Ignored)).MustHaveHappened();
    }

    [TestMethod]
    public void SaveCommand_ShouldCommitInRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var vm = new MainViewModel(repository, navigationService);
        vm.SaveCommand.Execute(null);
        A.CallTo(() => repository.Commit()).MustHaveHappened();
    }

    [TestMethod]
    public void SearchCommand_WithText_ShouldSetFilter()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.SearchCommand.Execute("text");
        var coll = CollectionViewSource.GetDefaultView(vm.Customers);
        coll.Filter.Should().NotBeNull();
    }

    [TestMethod]
    public void SearchCommand_WithoutText_ShouldSetFilter()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        vm.SearchCommand.Execute("");
        var coll = CollectionViewSource.GetDefaultView(vm.Customers);
        coll.Filter.Should().BeNull();
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldCallNavigate()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        A.CallTo(() => navigationService.Navigate(customerVM)).MustHaveHappened();
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldIncrementWindowCount()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        vm.WindowCount.Should().Be(1);
    }

    [TestMethod]
    public void ShowDetailsCommand_ShouldAddToOpenWindows()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        vm.OpenWindows.Count.Should().Be(1);
        vm.OpenWindows[0].Should().Be(customerVM);
    }

    [TestMethod]
    public void CustomerCloseCommand_ShouldDecreaseWindowCount()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.ClosingCommand.Execute(null);
        vm.WindowCount.Should().Be(0);
    }

    [TestMethod]
    public void CustomerCloseCommand_ShouldRemoveFromOpenWindows()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.ClosingCommand.Execute(null);
        vm.OpenWindows.Count.Should().Be(0);
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldCallNavigationClose()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        A.CallTo(() => navigationService.Close(customerVM)).MustHaveHappened();
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldRemoveCustomer()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        vm.Customers.Count.Should().Be(9);
        vm.Customers.Should().NotContain(customerVM);
    }

    [TestMethod]
    public void CustomerDeleteCommand_ShouldCallRemoveFromRepository()
    {
        var repository = A.Fake<ICustomerRepository>();
        var navigationService = A.Fake<INavigationService>();
        var customers = A.CollectionOfFake<Customer>(10);
        A.CallTo(() => repository.Customers).Returns(customers);
        var vm = new MainViewModel(repository, navigationService);
        CustomerViewModel customerVM = vm.Customers[1];
        vm.ShowDetailsCommand.Execute(customerVM);
        customerVM.DeleteCommand.Execute(null);
        A.CallTo(() => repository.Remove(A<Customer>.Ignored)).MustHaveHappened();
    }
}

If you run the tests, all will pass.

As you can see, we have decoupled the two ViewModels with the Messenger implemented in the MVVM Toolkit, our code is completely testable and it runs fine. You can check this code here.

We can still go one step further (yes, we can always do that 😃): inject the IMessenger in the constructor of the ViewModels.

In App.xaml.cs we add the registration for IMessenger:

private static IServiceProvider ConfigureServices()
{
    var services = new ServiceCollection();

    services.AddSingleton<INavigationService, NavigationService>();
    services.AddSingleton<ICustomerRepository, CustomerRepository>();
    services.AddSingleton<IMessenger>(WeakReferenceMessenger.Default);
    services.AddSingleton<MainViewModel>();

    return services.BuildServiceProvider();
}

And inject it in the constructor of MainViewModel and CustomerViewModel:

public MainViewModel(ICustomerRepository customerRepository, 
    INavigationService navigationService,
    IMessenger messenger)
{
    _customerRepository = customerRepository ?? 
                          throw new ArgumentNullException("customerRepository");
    _navigationService = navigationService ?? 
        throw new ArgumentNullException("navigationService"); 
    _messenger = messenger ??
        throw new ArgumentNullException("messenger");
    Customers = new ObservableCollection<CustomerViewModel>(
        _customerRepository.Customers.Select(c => new CustomerViewModel(c, messenger)));
    messenger.Register<WindowClosedMessage>(this, (r, m) =>
    {
        WindowCount--;
        _openWindows.Remove(m.Value);
    });
    messenger.Register<ViewModelDeletedMessage>(this, (r, m) =>
    {
        DeleteCustomer(m.Value);
    });
}
private readonly IMessenger _messenger;

public CustomerViewModel(Customer customer, IMessenger messenger)
{
    if (customer == null)
        throw new ArgumentNullException("customer");
    if (messenger == null)
        throw new ArgumentNullException("messenger");
    _messenger = messenger;
    _customer = customer;
    CustomerId = _customer.CustomerId;
    CompanyName = _customer.CompanyName;
    ContactName = _customer.ContactName;
    ContactTitle = _customer.ContactTitle;
    Address = _customer.Address;
    City = _customer.City;
    Region = _customer.Region;
    PostalCode = _customer.PostalCode;
    Country = _customer.Country;
    Phone = _customer.Phone;
    Fax = _customer.Fax;
}

We have decoupled the ViewModels from the default messenger. There are some changes to do in the code and in the tests, but you can check them in the final project, here. The code now is decoupled, testable and works fine 😃

All the source code for this article is at https://github.com/bsonnino/MessengerToolkit

I was working with the WPF project, converted to UWP in this article, in order to check usage of the new MVVM toolkit, explained in my last article and use the bindings with x:Bind, that are not supported in WPF (in fact, they are, but not natively. You can add the CompiledBindings.WPF package to your WPF project to have them).
I converted it, changed the Views to use the x:Bind bindings and ended up with this project.
There are some notes, here with the conversion:

  • This is an UWP project and, by default, uses C# 7. I wanted to use C# 9 t get the source generator goodies in the MVVM Toolkit. So I upgraded the language version using the LangVersion tag in the project. To do this, open the csproj file (you must unload the project and edit the file) and add this PropertyGroup:
<PropertyGroup>
  <LangVersion>9.0</LangVersion>
</PropertyGroup>
  • I removed the details view, by adding a ContentControl with a template with the Customer details:
<ContentControl Grid.Row="2" Content="{x:Bind master.SelectedItem, Mode=OneWay}" 
                ContentTemplate="{StaticResource DetailTemplate}" Margin="5" />
<Page.Resources>
    <c:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
    <DataTemplate x:Key="DetailTemplate" x:DataType="customerlib:Customer">
        <Grid HorizontalAlignment="Stretch">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="600" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <TextBlock Text="Customer Id:" Grid.Column="0" Grid.Row="0" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="0" Margin="3" Name="customerIdTextBox" 
                     Text="{x:Bind CustomerId, Mode=TwoWay}" VerticalAlignment="Center" />
            <FontIcon Grid.Column="2" FontFamily="Segoe MDL2 Assets" Glyph="" 
                      Margin="20,0" Visibility="{x:Bind IsVip, Converter={StaticResource BooleanToVisibilityConverter}}" />
            <TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" 
                     Text="{x:Bind CompanyName, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" 
                     Text="{x:Bind ContactName, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Contact Title:" Grid.Column="0" Grid.Row="3" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="3" Margin="3" Name="contactTitleTextBox" 
                     Text="{x:Bind ContactTitle, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Address:" Grid.Column="0" Grid.Row="4" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="4" Margin="3" Name="addressTextBox" 
                     Text="{x:Bind Address, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="City:" Grid.Column="0" Grid.Row="5" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="5" Margin="3" Name="cityTextBox" 
                     Text="{x:Bind City, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Postal Code:" Grid.Column="0" Grid.Row="6" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="6" Margin="3" Name="postalCodeTextBox" 
                     Text="{x:Bind PostalCode, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Region:" Grid.Column="0" Grid.Row="7" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="7" Margin="3" Name="regionTextBox" 
                     Text="{x:Bind Region, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Country:" Grid.Column="0" Grid.Row="8" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="8" Margin="3" Name="countryTextBox" 
                     Text="{x:Bind Country, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Phone:" Grid.Column="0" Grid.Row="9" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="9" Margin="3" Name="phoneTextBox" 
                     Text="{x:Bind Phone, Mode=TwoWay}" VerticalAlignment="Center" />
            <TextBlock Text="Fax:" Grid.Column="0" Grid.Row="10" Margin="3" VerticalAlignment="Center" />
            <TextBox Grid.Column="1" Grid.Row="10" Margin="3" Name="faxTextBox" 
                     Text="{x:Bind Fax, Mode=TwoWay}" VerticalAlignment="Center" />
        </Grid>
    </DataTemplate>
</Page.Resources>
  • The bindings for the DataGrid columns couldn’t be replaced by x:Bind. I could do it by replacing the DataGridTextColumn with DataGridTemplateColumn like in:
<controls:DataGridTemplateColumn Header="Customer ID">
    <controls:DataGridTemplateColumn.CellTemplate>
        <DataTemplate x:DataType="customerlib:Customer">
            <TextBlock Text="{x:Bind CustomerId, Mode=TwoWay}" />
        </DataTemplate>
    </controls:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn> 

But I thought it didn’t worth the extra code and I left it with the bindings. If you find some way to replace the Bindings in the DataGridTextColumn for x:Bind, please leave it in the comments.

You would ask, why change the Binding for x:Bind. There are some good reasons for that:

  • x:Bind is resolved at compile time, while Binding is resolved at runtime, so it should make your code run faster
  • By being resolved at compile time, you know in advance if some binding works or not, while with Binding, you will have something that doesn’t work and you don’t know why
  • You can bind to functions in your ViewModel, thus removing the need of some converters (but not all of them)

On the other side, you have to determine explicitely the data you’re binding with x:Bind:

  • The data templates must have a DataType
  • You cannot use the DataContext property of the View, as it’s an Object and, as so, doesn’t have the properties to bind
  • You must bind to properties/fields in the View. That’s why I created this code in Mainpage.xaml.cs:
private MainViewModel _mainVm;
public MainPage()
{
    this.InitializeComponent();
    _mainVm = App.Current.MainVM;
}
  • You must declare explicitely which property/field you are binding, like in
ItemsSource="{x:Bind _mainVm.Customers}"

With that, we can run the project and this is what you get:

It’s an empty window, the customer data is not there. I tried to figure out what was happening and I set a breakpoint in CustomerRepository.GetCustomersAsync:

Everything is good, there are 91 customers, but none of them is shown in the Datagrid. Why is that? After checking and rechecking the data and the bindings, I remembered one difference between Binding and x:Bind. While Binding uses the OneWay most of the time, x:Bind uses OneTime.

It seems to be a small difference, but it bytes you when you have to deliver some code 😦. OneWay is a binding from the source (the class) to the destination (the view), but every time the class changes its value and raises the PropertyChanged event, the view reflects the change. OneTime is there for performance reasons: it’s set and forget. When the view is first rendered, it will check the binding, set the value and forget. The changes on the class won’t affect the view.

As we are setting the customer’s list in an asynchronous way, when the view is rendered, there is no data available, so it doesn’t show anything. When the data is set, all changes aren’t propagated to the view and nothing is shown. Fortunately, this is a simple change:

<controls:DataGrid AutoGenerateColumns="False" x:Name="master" Grid.Row="1" 
                   ItemsSource="{x:Bind _mainVm.Customers, Mode=OneWay}" SelectedItem="{x:Bind _mainVm.SelectedCustomer, Mode=TwoWay}">

Now, our data shows fine, but when we select a customer, we get this:

Wait a minute, where are our bindings? We just got the first one and nothing else? What is happening here? Is x:Bind broken? Let’s see. I started to change my x:Bind to Binding and see the effect:

<TextBlock Text="Company Name:" Grid.Column="0" Grid.Row="1" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="1" Margin="3" Name="companyNameTextBox" 
         Text="{Binding CompanyName, Mode=TwoWay}" VerticalAlignment="Center" />
<TextBlock Text="Contact Name:" Grid.Column="0" Grid.Row="2" Margin="3" VerticalAlignment="Center" />
<TextBox Grid.Column="1" Grid.Row="2" Margin="3" Name="contactNameTextBox" 
         Text="{Binding ContactName, Mode=TwoWay}" VerticalAlignment="Center" />


I got these bindings working, so x:Bind must be broken, I thought. So, my idea of using x:Bind was broken and I started to change the x:Bind for Binding. That worked fine until I changed this one:

<FontIcon Grid.Column="2" FontFamily="Segoe MDL2 Assets" Glyph="" 
          Margin="20,0" Visibility="{Binding IsVip, Converter={StaticResource BooleanToVisibilityConverter}}" />

When I changed it, I got this runtime error

Wait a minute – Cannot find a Resource with the Name/Key BooleanToVisibilityConverter ? I’m sure I’ve declared it in the Resources section:

<c:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />

Oops – my bad! It’s BoolToVisibilityConverter, not BooleanToVisibilityConverter. I corrected the error and all the bindings started to work with x:Bind:

So, in some way, x:Bind has a bug, but not in the way I expected: instead of pointing the faulty converter with red squiggles, it will crash the bindings that come after it and won’t work anymore. That’s why, it was showing just the customer Id, the star for the IsVip property and nothing else. And it was showing the error at runtime, but I didn’t see it. In the Output window, in the middle of many other messages, a two line message indicated the error:

I think that, being checked as compile time, the converters should also be checked at compile time and not throw an exception in the output window at runtime and blow the rest of the bindings. But that’s just my point of view. Now, everything works and I will make sure that my converters are well defined 😃.

The source code for this project is at https://github.com/bsonnino/ConvertersInUWP

Sometime ago, I’ve written this article introducing the MVVM Community Toolkit and developing a CRUD application to show how to use the MVVM pattern with the Community toolkit.

The time has passed and version 8.0 of the MVVM Community toolkit has been released and, with it, a rewrite using incremental generators. This may seem a minor update, but it’s a huge move, as the MVVM pattern is full of boilerplate: implementing the INotifyPropertyChanged interface for the viewmodels, binding commands that implement the ICommand interface, using the RelayCommand class and implementing observable properties that raise the PropertyChanged event when changed. All that make the code cumbersome and repetitive, but that’s what we had to do to implement the MVVM pattern in our apps. Until now.

With the use of source generators, the toolkit removes a lot of the boilerplate and makes the code easier to create and read. In this article, we’ll take the project that we developed in the previous article and will change it to use the new toolkit.

You can clone the code in https://github.com/bsonnino/MvvmApp and open the CustomerApp – Mvvm app in Visual Studio 2022. As the original project is targeted to .NET 5.0, we’ll upgrade it to .NET 6.0. This is an easy task: just open CustomerApp.csproj and change the TargetFramework to .net6.0-windows:

<PropertyGroup>
	<OutputType>WinExe</OutputType>
	<TargetFramework>net6.0-windows</TargetFramework>
	<UseWPF>true</UseWPF>
	<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
</PropertyGroup>

Do the same with CustomerApp.Tests.csproj. There is no need to do it on the CustomerLib project, as it’s a Net Standard project.

The next step is to update the NuGet packages. The package Microsoft.Extensions.DependencyInjection must be upgraded to version 6.0.0. If you try to upgrade the Microsoft.Toolkit.Mvvm package, you’ll see that there is no upgrade to version 8.0. That’s because the package name has changed and you must uninstall this one and install the CommunityToolkit.Mvvm package.

Now, the project is ready to build and, when you do that, you’ll see that it doesn’t work 😦. This is because the namespaces have changed and we need to update the using clauses in MainViewModel.cs. We have to remove the old using clauses and replace with the new ones:

using Microsoft.Toolkit.Mvvm.ComponentModel;
using Microsoft.Toolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

Once you do that and recompile the project, you’ll see that it compiles fine and runs in the same way the original project did. Not bad for a project upgraded from .NET 5 to .NET 6, with a new version of the MVVM framework.

But did I say that with the new toolkit you can remove the boilerplate? We can start doing that now. The first thing is to remove the properties and their getters and setters. We decorate the _selectedCustomer field with the [ObservableProperty] attribute:

[ObservableProperty]
private Customer _selectedCustomer;

When you do that, you’ll see that the class name has a red underline:

That’s because when we add the attribute, the toolkit generates a partial class, and we need to add the partial keyword to the class:

public partial class MainViewModel : ObservableObject

When we add that, SelectedCustomer is underlined:

That’s because we have declared the property in our code and the toolkit has also declared the same property in the generated code. We can now remove the property declaration from our code:

public Customer SelectedCustomer
{
    get => _selectedCustomer;
    set
    {
        SetProperty(ref _selectedCustomer, value);
        RemoveCommand.NotifyCanExecuteChanged();
    }
}

As you can see from the code we are removing, the setter notifies the RemoveCommand. To have the same effect, we add the NotifyCanExecuteChangedFor attribute to the field:

[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
private Customer _selectedCustomer;

If you compile the code, you will see that it runs the same way it did before, and we are still using the property name (SelectedCommand) in the commands, even if it’s not explicitly defined in the code. That’s because the toolkit is generating the property in its partial part of the class.

The next steps are to remove the boilerplate from the commands in the code. For that, we must remove all declarations and leave only the command methods, changing their name to the command name (without Command at the end) and adding the [RelayCommand] attribute for the method. For the Add command, we have to change this code:

public IRelayCommand AddCommand { get; }

private void DoAdd()
{
    var customer = new Customer();
    _customerRepository.Add(customer);
    SelectedCustomer = customer;
    OnPropertyChanged("Customers");
}

To this code:

[RelayCommand]
private void Add()
{
    var customer = new Customer();
    _customerRepository.Add(customer);
    SelectedCustomer = customer;
    OnPropertyChanged("Customers");
}

We also have to remove the initialization code:

AddCommand = new RelayCommand(DoAdd);
RemoveCommand = new RelayCommand(DoRemove, () => SelectedCustomer != null);
SaveCommand = new RelayCommand(DoSave);
SearchCommand = new RelayCommand<string>(DoSearch);

If you notice the removed code, you’ll see that the RemoveCommand has a CanExecute method. To solve that, we have to have to add a parameter to RelayCommand:

[RelayCommand(CanExecute = "HasSelectedCustomer")]
private void Remove()
{

The parameter points to HasSelectedCustomer, a method that should be defined in the code:

private bool HasSelectedCustomer() => SelectedCustomer != null;

With that, we have completed our code and now the project runs in the same way it did before. The code is simpler and with no boilerplate:

public partial class MainViewModel : ObservableObject
{
    private readonly ICustomerRepository _customerRepository;

    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RemoveCommand))]
    private Customer _selectedCustomer;

    public MainViewModel(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository ??
                              throw new ArgumentNullException("customerRepository");
    }

    public IEnumerable<Customer> Customers => _customerRepository.Customers;

    [RelayCommand]
    private void Add()
    {
        var customer = new Customer();
        _customerRepository.Add(customer);
        SelectedCustomer = customer;
        OnPropertyChanged("Customers");
    }

    [RelayCommand(CanExecute = "HasSelectedCustomer")]
    private void Remove()
    {
        if (SelectedCustomer != null)
        {
            _customerRepository.Remove(SelectedCustomer);
            SelectedCustomer = null;
            OnPropertyChanged("Customers");
        }
    }

    private bool HasSelectedCustomer() => SelectedCustomer != null;

    [RelayCommand]
    private void Save()
    {
        _customerRepository.Commit();
    }

    [RelayCommand]
    private void Search(string textToSearch)
    {
        var coll = CollectionViewSource.GetDefaultView(Customers);
        if (!string.IsNullOrWhiteSpace(textToSearch))
            coll.Filter = c => ((Customer)c).Country.ToLower().Contains(textToSearch.ToLower());
        else
            coll.Filter = null;
    }
}

As you can see, this new version brought a huge improvement. We can use the MVVM pattern with no issues, there is no extra code related to the pattern (except for the attributes) and the code is easier to read and follow. And all tests still run, with no change at all.

The full source code for this article is at https://github.com/bsonnino/MVVMToolkit8