Using toast notifications in Windows Forms
Tags: ux, dotnet, csharp, codenewbie
Published: 2023-06-19 19:36:35

The purpose of a toast allows users to know that their action was acknowledged or that something happened like an email has arrived in a polite manner.

To create toast notifications we will use the Windows Community Toolkit with the following NuGet package [Microsoft.Toolkit.Uwp.Notifications](https://www.nuget.org/packages/Microsoft.Toolkit.Uwp.Notifications/7.1.3?_src=template).

There is the following, [Send a local toast notification from a C# app](https://learn.microsoft.com/en-us/windows/apps/design/shell/tiles-and-notifications/send-local-toast?tabs=uwp#set-an-expiration-time) which will walkthrough how to create toast notifications which is targeted for experienced developers but for the novice developer which does not take time to read the instructions will be a painful experience as there are countless questions on the web that show these coders have not fully read the documentation.

## Intentions

- Provide clear instructions on how to create toast
- Provide source code
- Go past the basics as most documentation does not cover Windows Forms

## Basic toast

Rather than adding a toast to an existing project which may interfere with creating toast it is better to learn from creating a new project.

Create a new Windows forms core project targeting .NET Core 6 or higher (provided source code uses .NET Core 7). The name of the project is not important but know that when using toast the title will be the name of the project, I will show how to correct this shortly in the project file.

Next, double click on the project file in solution explorer

This is the standard project file.

```xml
<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>WinExe</OutputType>
        <TargetFramework>net7.0-windows</TargetFramework>
        <Nullable>enable</Nullable>
        <UseWindowsForms>true</UseWindowsForms>
        <ImplicitUsings>enable</ImplicitUsings>
    </PropertyGroup>

</Project>
```

Replace

```xml
<TargetFramework>net7.0-windows</TargetFramework>
```

With the following

```xml
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
```

Giving toast notifications a title. Add the following below the last line we added above.

```xml
<Product>TODO</Product>
```

Replace TODO with a title for toast, in the code sample the following is used.

```xml
<Product>Notifications 2023</Product>
```

### Creating a simple toast

Add the following using to the form.

```csharp
using Microsoft.Toolkit.Uwp.Notifications;
```

Add a button to form1, double click and add the following code.

```csharp
new ToastContentBuilder()
    .AddText("This is a simple notification")
    .Show();
```

Build, run, click the button for a toast and the toast displays.


![basic toast](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ovk20eg6zdgohjl5eck5.png)

Change the code to add another line of text.

```csharp
new ToastContentBuilder()
    .AddText("This is a simple notification")
    .AddText("Some more text")
    .Show();
```
Build, run, click the button for a toast and the toast displays. Note the second line is muted.

![basic toast with two lines of text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8sm37tx5mk7cxl81x7z4.png)

> **Note**
> Keep the text short and too the point as most notifications are short lived although if more information is needed set an expiration.

In Windows 10, all toast notifications go in Action Center after they are dismissed or ignored by the user, so users can look at your notification after the popup is gone.

However, if the message in your notification is only relevant for a period of time, you should set an expiration time on the toast notification so the users do not see stale information from your app.

Example to have an expiration of one day

```csharp
new ToastContentBuilder()
    .AddText("This is a simple notification")
    .Show(toast =>
    {
        toast.ExpirationTime = DateTime.Now.AddDays(1);
    });
```

## Real world download a file

Suppose your application needs to download a file which can take some time, this is a good use for a toast. If there is other work the user can do while processing the download or they leave and come back the toast is both shown and seen in the Windows Action center.

Add the following class to the project, PublicHoliday.cs

```csharp
public class PublicHoliday
{
    public DateTime Date { get; set; }
    public string LocalName { get; set; }
    public string Name { get; set; }
    public string CountryCode { get; set; }
    public bool Fixed { get; set; }
    public bool Global { get; set; }
    public string[] Counties { get; set; }
    public int? LaunchYear { get; set; }
    public string[] Types { get; set; }

    private sealed class DateEqualityComparer : IEqualityComparer<PublicHoliday>
    {
        public bool Equals(PublicHoliday x, PublicHoliday y)
        {
            if (ReferenceEquals(x, y)) return true;
            if (ReferenceEquals(x, null)) return false;
            if (ReferenceEquals(y, null)) return false;
            if (x.GetType() != y.GetType()) return false;
            return x.Date.Equals(y.Date);
        }

        public int GetHashCode(PublicHoliday obj) => obj.Date.GetHashCode();

    }

    public static IEqualityComparer<PublicHoliday> DateComparer { get; } = new DateEqualityComparer();

    public override string ToString() => Name;

}
```

Add another class, ToastOperations.cs which is for displaying toast for a successful download or a failed download.

```csharp
using Microsoft.Toolkit.Uwp.Notifications;
using System;

namespace WinFormsApp1;
internal class ToastOperations
{
    public static void HolidaysDownloaded()
    {
        new ToastContentBuilder()
            .AddText("Go back to app")
            .AddHeader("Holidays1", "Holidays download", "")
            .AddButton(new ToastButton().SetContent("OK"))
            .SetToastScenario(ToastScenario.Default)
            .Show(toast =>
            {
                toast.ExpirationTime = DateTime.Now.AddMinutes(2);
            });
    }
    public static void HolidaysDownloadFailed()
    {
        new ToastContentBuilder()
            .AddText("Holidays not download")
            .AddText("Email sent to developer")
            .AddButton(new ToastButton().SetContent("OK"))
            .SetToastScenario(ToastScenario.Alarm)
            .Show(toast =>
            {
                toast.ExpirationTime = DateTime.Now.AddMinutes(2);
            });
    }
}
```

Add the following class WorkOperations.cs which is responsible for downloading holidays for a specific country.

```csharp
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;

namespace WinFormsApp1;
public class WorkOperations
{
    public static async Task<List<PublicHoliday>> GetHolidays(string countryCode = "US")
    {
    
        var jsonSerializerOptions = new JsonSerializerOptions
        {
            PropertyNameCaseInsensitive = true
        };

        using var httpClient = new HttpClient();

        var response = await httpClient.GetAsync(
            
$"""
https://date.nager.at/api/v3/publicholidays/{DateTime.Now.Year}/{countryCode}
""");

        if (response.IsSuccessStatusCode)
        {
            await using Stream jsonStream = await response.Content.ReadAsStreamAsync();

            // Distinct is used as there were duplicate entries
            return JsonSerializer.Deserialize<PublicHoliday[]>(
                    jsonStream, jsonSerializerOptions)
                    !.Distinct(PublicHoliday.DateComparer).ToList();
        }
        else
        {
            return Enumerable.Empty<PublicHoliday>().ToList();
        }
    }
}

```

Back in the form, replace current code with the following.

```csharp
private async void button1_Click(object sender, System.EventArgs e)
{
    List<PublicHoliday> usHolidays = await WorkOperations.GetHolidays();
    if (usHolidays.Any())
    {
        ToastOperations.HolidaysDownloaded();
    }
    else
    {
        ToastOperations.HolidaysDownloadFailed();
    }
}
```

Run the app, click the button and get the following toast.


![toast for downloaded file](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rku8auhwnrjomdfpe8k2.png)

## Example with custom buttons

In this example we will display the following and write code to react to clicking the buttons.


![Toast as an a alarm](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j7tsna2qocb9b1cjz4fw.png)

First clone the repository and take the following images from Notification project and add them to the current project.

alarm.png and checkMark.png

Create a folder Images and place the images there along with setting Copy to Output Directory to copy if newer.

Add the following method to ToastOperations.cs

```csharp
public static void Alarm()
{
    var alarmPhoto = Path.Combine(
        AppDomain.CurrentDomain.BaseDirectory, "Images", @"alarm.png");
    var checkPhoto = Path.Combine(
        AppDomain.CurrentDomain.BaseDirectory, "Images", @"checkMark.png");

    new ToastContentBuilder()
        .AddArgument("action", "viewConversation")
        .AddArgument("conversationId", Dictionary["key3"])
        .AddText("Time for work")
        .AddButton(new ToastButton()
            .SetContent("OK")
            .AddArgument("action", "work")
            .SetImageUri(new Uri(checkPhoto)))
        .AddButton(new ToastButton()
            .SetContent("Snooze")
            .AddArgument("action", "snooze")
            .SetImageUri(new Uri(alarmPhoto)))
        .SetToastScenario(ToastScenario.Alarm)
        .Show();
}
```

- First two **AddArgument** are used to identify the operation
- Two buttons have a **AddArgument** which identifies which button was clicked.
- **.SetToastScenario(ToastScenario.Alarm)** will invoke a sound.

There are four members for [ToastScenario](https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.uwp.notifications.toastscenario?view=win-comm-toolkit-dotnet-7.1).

Next, the following code monitors toast notifications, add the following code to ToastOperations.cs

```csharp
public static Dictionary<string, int> Dictionary { get; } = new()
{
    { "key1", 100 }, // hero button, send user to a GitHub repository
    { "key2", 200 }, // Intercept button
    { "key3", 300 }, // alarm button
    { "key4", 400 },  // Favorite color button for text box
    { "key5", 500 },
    { "key6", 600 },
};

public static string MainKey => "conversationId";

public static void OnActivated()
{
    ToastNotificationManagerCompat.OnActivated += toastArgs =>
    {
        ToastArguments args = ToastArguments.Parse(toastArgs.Argument);

        if (args.Contains(MainKey))
        {
            if (args[MainKey] == Dictionary["key3"].ToString())
            {
                if (args.Contains("action"))
                {
                    if (args["action"] == "snooze")
                    {
                        WorkOperations.Snooze();
                    }
                    else if (args["action"] == "work")
                    {
                        WorkOperations.GotoWork();
                    }
                }
            }
        }
    };

}
```
To use OnActivated, open Program.cs and replace its contents with the following and make sure you update the namespace to match your project.

- Init() will be the first code called as per [ModuleInitializer](https://khalidabuhakmeh.com/module-initializers-in-csharp-9)

```csharp
using System;
using System.Runtime.CompilerServices;
using System.Windows.Forms;

namespace WinFormsApp1;

internal static class Program
{

    [STAThread]
    static void Main()
    {
        Application.Run(new Form1());
    }
    [ModuleInitializer]
    public static void Init()
    {
        ApplicationConfiguration.Initialize();

        ToastOperations.OnActivated();
    }
}
```

Now run the project, click the button, toast is shown, try each button. Note that the results are displayed in Visual Studio's output window, for a real application take appropriate actions in code.


![output window results](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jd7b24lbbq2366j0htlv.png)

## Advance

> **Note**
> The following example are included in the source code provided.

There may be times when an operation requires user input, for example, ask a question and need a text response.

For this there is [ToastButton](https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.uwp.notifications.toastcontentbuilder.addbutton?view=win-comm-toolkit-dotnet-7.1).

In ToastOperations.cs in Notification project
```csharp
public static void TextBoxFavoriteColor()
{
    new ToastContentBuilder()
        .AddArgument("conversationId", Dictionary["key4"])
        .AddText("Question")
        .AddInputTextBox("favoriteColor",
            placeHolderContent: "Type a response",
            title: "What is your favorite color")
        .AddButton(new ToastButton()
            .SetContent("Give it to me"))
        .Show();
}
```

Invoked

```csharp
ToastOperations.TextBoxFavoriteColor();
```

Captured in **public static void OnActivated()** and note Log is [SeriLog](https://serilog.net/).

```csharp
}else if (args[MainKey] == Dictionary["key4"].ToString())
{
    ValueSet? valueSet = toastArgs.UserInput;
    
    if (!valueSet.Keys.Contains("favoriteColor")) return;

    var favoriteColor = valueSet["favoriteColor"].ToString();
    if (!string.IsNullOrWhiteSpace(favoriteColor))
    {
        Log.Information($"favorite color: {favoriteColor}");
    }
}
```

At run time.

![text input example](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pxlsxxwbg3ucjp9zs6d2.png)

Another case is for predefined options. 


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w79p0xq39j5xenabvqwf.png)

The following presents selections where [ToastSelectionBoxItem](https://learn.microsoft.com/en-us/dotnet/api/microsoft.toolkit.uwp.notifications.toastselectionboxitem?view=win-comm-toolkit-dotnet-7.1) accepts an **id** and **text** to display. The **id** is used to identify which selection was selected.

```csharp
public static void SelectionBox()
{
    new ToastContentBuilder()
        .AddArgument("conversationId", Dictionary["key5"])
        .AddText("You computer must restart")
        .AddText("Select when")
        // id, time is used above in OnActivated
        .AddToastInput(new ToastSelectionBox("time")
        {
            DefaultSelectionBoxItemId = "0",
            // note only five items are permitted
            Items =
            {
                new ToastSelectionBoxItem("0", "Now"),
                new ToastSelectionBoxItem("15", "15 minutes"),
                new ToastSelectionBoxItem("30", "30 minutes"),
                new ToastSelectionBoxItem("45", "45 minutes"),
                new ToastSelectionBoxItem("60", "1 hour"),
            }

        })
        .AddButton(new ToastButton().SetContent("OK"))
        .SetToastScenario(ToastScenario.Reminder)
        .Show();
}
```

In OnActivate

```csharp
else if (args[MainKey] == Dictionary["key5"].ToString())
{
    ValueSet? valueSet = toastArgs.UserInput;
    if (!valueSet.Keys.Contains("time")) return;
    int time = Convert.ToInt32(valueSet["time"]);
    var item = TimeOperations.TimeList()
        .FirstOrDefault(x => x.Id == time);
    item.Action();

}
```

TimeOperations.TimeList() has a list of Time found in ToastLibrary class project.

```csharp
public class Time
{
    public int Id { get; set; }
    /// <summary>
    /// Operation to perform
    /// </summary>
    public Action Action;
}
```

Where there is an item for each time in SelectionBox() above.

```csharp
public class TimeOperations
{
    /// <summary>
    /// For demonstrating working with ToastSelectionBoxItem in
    /// ToastOperations.
    ///
    /// In a real application the Action would perform a meaningful task
    /// 
    /// </summary>
    public static List<Time> TimeList() =>
        new()
        {
            new () { Id =  0, Action = () => Information("Now") },
            new () { Id = 15, Action = () => Information("15 minutes") },
            new () { Id = 30, Action = () => Information("30 minutes")  },
            new () { Id = 45, Action = () => Information("45 minutes")  },
            new () { Id = 60, Action = () => Information("60 minutes")  }
        };
}
```

For a match item.Action(); executes an action, here it display some text to Visual Studio output window while in a real application the action would be meaningful to the question asked.

## Provided code

- [ToastLibrary](https://github.com/karenpayneoregon/toast-notifications/tree/master/ToastLibrary), contains all toast notification code
- [Notification](https://github.com/karenpayneoregon/toast-notifications/tree/master/Notifications), front end which utilizes ToastLibrary.

In several of the code samples [FluentScheduler](https://fluentscheduler.github.io/) is used to schedule code to run.

In Notifications project, Program.cs the following line initializes FluentScheduler

```csharp
JobManager.Initialize(new JobsRegistry());
```
JobsRegistry is located in ToastLibrary.

```csharp
public class JobsRegistry : Registry
{
    public JobsRegistry()
    {
        // Toast notification

        // run once every minute while app is running
        Schedule(ApplicationJobs.AnnoyingToastNotification)
            .WithName("Annoying")
            .ToRunEvery(1).Minutes();

        Schedule(ApplicationJobs.PollTaxpayers)
            .WithName("PollTaxpayers")
            .ToRunEvery(5).Seconds();

        // run once three minutes after app starts
        DateTime dateTime = DateTime.Now.AddMinutes(3);
        Schedule(ApplicationJobs.OnceToastNotification)
            .WithName("Annoying").ToRunOnceAt(dateTime);
    }
}
```
Back in Program.cs, the following lines of code show when a job starts and finishes.

```csharp
JobManager.JobStart += info => 
    Log.Information($"{info.Name}: started");
JobManager.JobEnd += info => 
    Log.Information($"{info.Name}: ended ({info.Duration})");
```

Now let's look at the provide jobs.

- AnnoyingToastNotification is called every three minutes and displays a notification. Of course as is is useless but use your imagination.
- OnceToastNotification displays a notification one time.
- PollTaxpayers gets a count of records in a database and alerts the user if there are more than 1,000 records. 

For **PollTaxpayers** see the following code from Notifications project.

Which provides an option to truncate the database table to retry the operation to get a notification or while truncated no notification.

```csharp
private async void TaxpayerButton_Click(object sender, EventArgs e)
{
    if (await DataOperations.DatabaseExist())
    {
        if (Question("Truncate Taxpayer table", "Question"))
        {
            DataOperations.TruncateTable();
        }

        var (success, exception) = await DataOperations.AddNewTaxpayers(BogusOperations.Taxpayers());
        if (exception is not null)
        {
            // no toast here, we want this in the user's face
            MessageBox.Show($@"Operation failed {exception.Message}");
        }
        else
        {
            ToastOperations.FinishedAddingRecords();
        }
    }
    else
    {
        MessageBox.Show("Database not found");
    }

}
```

## Source code

Clone the following [GitHub repository](https://github.com/karenpayneoregon/toast-notifications).

There are several web projects which will be addressed in another article.

## Requires

- Microsoft Visual Studio 2022 or higher
- .NET Core 7 installed
- Windows 10 or higher