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>
Replace
<TargetFramework>net7.0-windows</TargetFramework>
With the following
<TargetFramework>net7.0-windows10.0.19041.0</TargetFramework>
Giving toast notifications a title. Add the following below the last line we added above.
<Product>TODO</Product>
Replace TODO with a title for toast, in the code sample the following is used.
<Product>Notifications 2023</Product>
Creating a simple toast
Add the following using to the form.
using Microsoft.Toolkit.Uwp.Notifications;
Add a button to form1, double click and add the following code.
new ToastContentBuilder()
.AddText("This is a simple notification")
.Show();
Build, run, click the button for a toast and the toast displays.
Change the code to add another line of text.
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.
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);
});
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;
}
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);
});
}
}
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();
}
}
}
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();
}
}
Run the app, click the button and get the following toast.
Example with custom buttons
In this example we will display the following and write code to react to clicking the buttons.
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();
}
- 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();
}
}
}
}
};
}
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
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.
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();
}
Invoked
ToastOperations.TextBoxFavoriteColor();
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}");
}
}
At run time.
Another case is for predefined options.
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();
}
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();
}
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;
}
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") }
};
}
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, contains all toast notification code
- Notification, front end which utilizes ToastLibrary.
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());
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);
}
}
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})");
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");
}
}
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