Using toast notifications in Windows Forms

karenpayneoregon

Karen Payne

Posted on June 19, 2023

Using toast notifications in Windows Forms

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.

There is the following, Send a local toast notification from a C# app 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.

<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>
Enter fullscreen mode Exit fullscreen mode

Replace

<TargetFramework>net7.0-windows</TargetFramework>
Enter fullscreen mode Exit fullscreen mode

With the following

<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
Enter fullscreen mode Exit fullscreen mode

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

<Product>TODO</Product>
Enter fullscreen mode Exit fullscreen mode

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

<Product>Notifications 2023</Product>
Enter fullscreen mode Exit fullscreen mode

Creating a simple toast

Add the following using to the form.

using Microsoft.Toolkit.Uwp.Notifications;
Enter fullscreen mode Exit fullscreen mode

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

new ToastContentBuilder()
    .AddText("This is a simple notification")
    .Show();
Enter fullscreen mode Exit fullscreen mode

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

basic toast

Change the code to add another line of text.

new ToastContentBuilder()
    .AddText("This is a simple notification")
    .AddText("Some more text")
    .Show();
Enter fullscreen mode Exit fullscreen mode

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

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

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

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

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;

}
Enter fullscreen mode Exit fullscreen mode

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

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);
            });
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

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

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

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

toast for downloaded file

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

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

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();
}
Enter fullscreen mode Exit fullscreen mode
  • 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.

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

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();
                    }
                }
            }
        }
    };

}
Enter fullscreen mode Exit fullscreen mode

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

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();
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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.

In ToastOperations.cs in Notification project

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();
}
Enter fullscreen mode Exit fullscreen mode

Invoked

ToastOperations.TextBoxFavoriteColor();
Enter fullscreen mode Exit fullscreen mode

Captured in public static void OnActivated() and note Log is SeriLog.

}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}");
    }
}
Enter fullscreen mode Exit fullscreen mode

At run time.

text input example

Another case is for predefined options.

Image description

The following presents selections where ToastSelectionBoxItem accepts an id and text to display. The id is used to identify which selection was selected.

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();
}
Enter fullscreen mode Exit fullscreen mode

In OnActivate

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();

}
Enter fullscreen mode Exit fullscreen mode

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

public class Time
{
    public int Id { get; set; }
    /// <summary>
    /// Operation to perform
    /// </summary>
    public Action Action;
}
Enter fullscreen mode Exit fullscreen mode

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

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")  }
        };
}
Enter fullscreen mode Exit fullscreen mode

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

In several of the code samples FluentScheduler is used to schedule code to run.

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

JobManager.Initialize(new JobsRegistry());
Enter fullscreen mode Exit fullscreen mode

JobsRegistry is located in ToastLibrary.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

Back in Program.cs, the following lines of code show when a job starts and finishes.

JobManager.JobStart += info => 
    Log.Information($"{info.Name}: started");
JobManager.JobEnd += info => 
    Log.Information($"{info.Name}: ended ({info.Duration})");
Enter fullscreen mode Exit fullscreen mode

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.

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");
    }

}
Enter fullscreen mode Exit fullscreen mode

Source code

Clone the following GitHub repository.

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
💖 💪 🙅 🚩
karenpayneoregon
Karen Payne

Posted on June 19, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related