Skip to content

Instantly share code, notes, and snippets.

@davidair
Last active August 12, 2022 06:39
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save davidair/c4ea207bf6eece4ef08b97ab29a3036f to your computer and use it in GitHub Desktop.
Save davidair/c4ea207bf6eece4ef08b97ab29a3036f to your computer and use it in GitHub Desktop.
Sample for self-registering desktop C# app that creates and reacts to toasts
# Test app for showing Windows Notifications and responding to them.
Copyright 2020 Google LLC.
SPDX-License-Identifier: MIT
## Requirements
The program uses the following packages:
1. Microsoft.Toolkit.Uwp.Notifications
Allows displaying Windows Action Center notifications
2. Microsoft.Windows.SDK.Contracts
Windows Runtime APIs
3. WindowsAPICodePack-Core
Defines the PropertyKey class
```
Install-Package Microsoft.Toolkit.Uwp.Notifications -Version 6.1.0
Install-Package WindowsAPICodePack-Core -Version 1.1.2
Install-Package Microsoft.Windows.SDK.Contracts -Version 10.0.19041.1
```
Additionally, it requires the following file:
https://raw.githubusercontent.com/WindowsNotifications/desktop-toasts/master/CS/DesktopToastsApp/DesktopNotificationManagerCompat.cs
## Documentation
### Toast guide for desktop apps
https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/send-local-toast-desktop
### Registering the app in code (as opposed to WiX)
https://docs.microsoft.com/en-us/windows/win32/shell/enable-desktop-toast-with-appusermodelid
### Format ids and property ids
https://docs.microsoft.com/en-us/windows/win32/properties/props-system-appusermodel-id
/**
* Copyright 2020 Google LLC.
* SPDX-License-Identifier: MIT
*/
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System.Windows.Forms;
using Windows.Data.Xml.Dom;
using Windows.UI.Notifications;
namespace Toasty
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
private void ShowToast()
{
string title = "Toasty!";
string content = "Check out the yummy toast";
string toastVisual =
$@"<visual>
<binding template='ToastGeneric'>
<text>{title}</text>
<text>{content}</text>
</binding>
</visual>";
string argsLaunch = $"action=doSomething";
string toastXmlString =
$@"<toast launch='{argsLaunch}'>
{toastVisual}
</toast>";
XmlDocument toastXml = new XmlDocument();
toastXml.LoadXml(toastXmlString);
var toast = new ToastNotification(toastXml);
DesktopNotificationManagerCompat.CreateToastNotifier().Show(toast);
}
private void buttonToast_Click(object sender, EventArgs e)
{
ShowToast();
}
}
}
/**
* Copyright 2020 Google LLC.
* SPDX-License-Identifier: MIT
*/
using Microsoft.Toolkit.Uwp.Notifications;
using System.Runtime.InteropServices;
using System.Windows.Forms;
[ClassInterface(ClassInterfaceType.None)]
[ComSourceInterfaces(typeof(INotificationActivationCallback))]
[Guid(Toasty.Program.ActivationId), ComVisible(true)]
public class MyNotificationActivator : NotificationActivator
{
public override void OnActivated(string invokedArgs, NotificationUserInput userInput, string appUserModelId)
{
MessageBox.Show("OnActicated " + invokedArgs);
}
}
/**
* Copyright 2020 Google LLC.
* SPDX-License-Identifier: MIT
*/
using Microsoft.Toolkit.Uwp.Notifications;
using System;
using System.IO;
using System.Reflection;
using System.Windows.Forms;
namespace Toasty
{
static class Program
{
public const string ActivationId = "c816b665-e067-4d1c-9a93-5ee7e5f0f03f";
private const string AppName = "DavidAir.Toasty";
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
string shortcutPath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.Programs),
"Toasty.lnk");
DesktopNotificationManagerCompat.RegisterAumidAndComServer<MyNotificationActivator>(AppName);
DesktopNotificationManagerCompat.RegisterActivator<MyNotificationActivator>();
if (!File.Exists(shortcutPath))
{
ShortcutManager.RegisterAppForNotifications(
shortcutPath, Assembly.GetExecutingAssembly().Location, null, AppName, ActivationId);
}
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run(new MainForm());
}
}
}
/**
* Copyright 2020 Google LLC.
* SPDX-License-Identifier: MIT
*/
using Microsoft.WindowsAPICodePack.Shell.PropertySystem;
using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
namespace Toasty
{
internal static class UnsafeNativeMethods
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct WIN32_FIND_DATAW
{
internal uint dwFileAttributes;
// ftCreationTime was a by-value FILETIME structure
internal uint ftCreationTime_dwLowDateTime;
internal uint ftCreationTime_dwHighDateTime;
// ftLastAccessTime was a by-value FILETIME structure
internal uint ftLastAccessTime_dwLowDateTime;
internal uint ftLastAccessTime_dwHighDateTime;
// ftLastWriteTime was a by-value FILETIME structure
internal uint ftLastWriteTime_dwLowDateTime;
internal uint ftLastWriteTime_dwHighDateTime;
internal uint nFileSizeHigh;
internal uint nFileSizeLow;
internal uint dwReserved0;
internal uint dwReserved1;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
internal string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
internal string cAlternateFileName;
}
/// <summary>IShellLink.Resolve fFlags</summary>
[Flags()]
internal enum SLR_FLAGS
{
/// <summary>
/// Do not display a dialog box if the link cannot be resolved. When SLR_NO_UI is set,
/// the high-order word of fFlags can be set to a time-out value that specifies the
/// maximum amount of time to be spent resolving the link. The function returns if the
/// link cannot be resolved within the time-out duration. If the high-order word is set
/// to zero, the time-out duration will be set to the default value of 3,000 milliseconds
/// (3 seconds). To specify a value, set the high word of fFlags to the desired time-out
/// duration, in milliseconds.
/// </summary>
SLR_NO_UI = 0x1,
/// <summary>Obsolete and no longer used</summary>
SLR_ANY_MATCH = 0x2,
/// <summary>If the link object has changed, update its path and list of identifiers.
/// If SLR_UPDATE is set, you do not need to call IPersistFile::IsDirty to determine
/// whether or not the link object has changed.</summary>
SLR_UPDATE = 0x4,
/// <summary>Do not update the link information</summary>
SLR_NOUPDATE = 0x8,
/// <summary>Do not execute the search heuristics</summary>
SLR_NOSEARCH = 0x10,
/// <summary>Do not use distributed link tracking</summary>
SLR_NOTRACK = 0x20,
/// <summary>Disable distributed link tracking. By default, distributed link tracking tracks
/// removable media across multiple devices based on the volume name. It also uses the
/// Universal Naming Convention (UNC) path to track remote file systems whose drive letter
/// has changed. Setting SLR_NOLINKINFO disables both types of tracking.</summary>
SLR_NOLINKINFO = 0x40,
/// <summary>Call the Microsoft Windows Installer</summary>
SLR_INVOKE_MSI = 0x80
}
[Flags()]
internal enum SLGP_FLAGS
{
/// <summary>Retrieves the standard short (8.3 format) file name</summary>
SLGP_SHORTPATH = 0x1,
/// <summary>Retrieves the Universal Naming Convention (UNC) path name of the file</summary>
SLGP_UNCPRIORITY = 0x2,
/// <summary>Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded</summary>
SLGP_RAWPATH = 0x4
}
[SuppressUnmanagedCodeSecurity]
[DllImport("ole32.dll")]
public extern static int PropVariantClear(ref PROPVARIANT pvar);
}
[StructLayout(LayoutKind.Sequential)]
internal struct CLIPDATA
{
public uint cbSize; //ULONG
public int ulClipFmt; //long
public IntPtr pClipData; //BYTE*
}
// Credit: http://blogs.msdn.com/b/adamroot/archive/2008/04/11/interop-with-propvariants-in-net.aspx
/// <summary>
/// Represents the OLE struct PROPVARIANT.
/// </summary>
/// <remarks>
/// Must call Clear when finished to avoid memory leaks. If you get the value of
/// a VT_UNKNOWN prop, an implicit AddRef is called, thus your reference will
/// be active even after the PropVariant struct is cleared.
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
internal struct PROPVARIANT
{
#region struct fields
// The layout of these elements needs to be maintained.
//
// NOTE: We could use LayoutKind.Explicit, but we want
// to maintain that the IntPtr may be 8 bytes on
// 64-bit architectures, so we'll let the CLR keep
// us aligned.
//
// NOTE: In order to allow x64 compat, we need to allow for
// expansion of the IntPtr. However, the BLOB struct
// uses a 4-byte int, followed by an IntPtr, so
// although the p field catches most pointer values,
// we need an additional 4-bytes to get the BLOB
// pointer. The p2 field provides this, as well as
// the last 4-bytes of an 8-byte value on 32-bit
// architectures.
// This is actually a VarEnum value, but the VarEnum type
// shifts the layout of the struct by 4 bytes instead of the
// expected 2.
ushort vt;
ushort wReserved1;
ushort wReserved2;
ushort wReserved3;
public IntPtr p;
int p2;
#endregion // struct fields
#region union members
sbyte cVal // CHAR cVal;
{
get { return (sbyte)GetDataBytes()[0]; }
}
byte bVal // UCHAR bVal;
{
get { return GetDataBytes()[0]; }
}
short iVal // SHORT iVal;
{
get { return BitConverter.ToInt16(GetDataBytes(), 0); }
}
ushort uiVal // USHORT uiVal;
{
get { return BitConverter.ToUInt16(GetDataBytes(), 0); }
}
int lVal // LONG lVal;
{
get { return BitConverter.ToInt32(GetDataBytes(), 0); }
}
uint ulVal // ULONG ulVal;
{
get { return BitConverter.ToUInt32(GetDataBytes(), 0); }
}
long hVal // LARGE_INTEGER hVal;
{
get { return BitConverter.ToInt64(GetDataBytes(), 0); }
}
ulong uhVal // ULARGE_INTEGER uhVal;
{
get { return BitConverter.ToUInt64(GetDataBytes(), 0); }
}
float fltVal // FLOAT fltVal;
{
get { return BitConverter.ToSingle(GetDataBytes(), 0); }
}
double dblVal // DOUBLE dblVal;
{
get { return BitConverter.ToDouble(GetDataBytes(), 0); }
}
bool boolVal // VARIANT_BOOL boolVal;
{
get { return (iVal == 0 ? false : true); }
}
int scode // SCODE scode;
{
get { return lVal; }
}
decimal cyVal // CY cyVal;
{
get { return decimal.FromOACurrency(hVal); }
}
DateTime date // DATE date;
{
get { return DateTime.FromOADate(dblVal); }
}
#endregion // union members
private byte[] GetBlobData()
{
var blobData = new byte[lVal];
IntPtr pBlobData;
try
{
switch (IntPtr.Size)
{
case 4:
pBlobData = new IntPtr(p2);
break;
case 8:
pBlobData = new IntPtr(BitConverter.ToInt64(GetDataBytes(), sizeof(int)));
break;
default:
throw new NotSupportedException();
}
Marshal.Copy(pBlobData, blobData, 0, lVal);
}
catch
{
return null;
}
return blobData;
}
internal CLIPDATA GetCLIPDATA()
{
return (CLIPDATA)Marshal.PtrToStructure(p, typeof(CLIPDATA));
}
/// <summary>
/// Gets a byte array containing the data bits of the struct.
/// </summary>
/// <returns>A byte array that is the combined size of the data bits.</returns>
private byte[] GetDataBytes()
{
var ret = new byte[IntPtr.Size + sizeof(int)];
if (IntPtr.Size == 4)
{
BitConverter.GetBytes(p.ToInt32()).CopyTo(ret, 0);
}
else if (IntPtr.Size == 8)
{
BitConverter.GetBytes(p2).CopyTo(ret, IntPtr.Size);
}
return ret;
}
/// <summary>
/// Called to clear the PropVariant's referenced and local memory.
/// </summary>
/// <remarks>
/// You must call Clear to avoid memory leaks.
/// </remarks>
public void Clear()
{
// Can't pass "this" by ref, so make a copy to call PropVariantClear with
PROPVARIANT var = this;
UnsafeNativeMethods.PropVariantClear(ref var);
// Since we couldn't pass "this" by ref, we need to clear the member fields manually
// NOTE: PropVariantClear already freed heap data for us, so we are just setting
// our references to null.
vt = (ushort)VarEnum.VT_EMPTY;
wReserved1 = wReserved2 = wReserved3 = 0;
p = IntPtr.Zero;
p2 = 0;
}
/// <summary>
/// Gets the variant type.
/// </summary>
public VarEnum Type
{
get { return (VarEnum)vt; }
}
/// <summary>
/// Gets the variant value.
/// </summary>
public object Value
{
get
{
switch ((VarEnum)vt)
{
case VarEnum.VT_I1:
return cVal;
case VarEnum.VT_UI1:
return bVal;
case VarEnum.VT_I2:
return iVal;
case VarEnum.VT_UI2:
return uiVal;
case VarEnum.VT_I4:
case VarEnum.VT_INT:
return lVal;
case VarEnum.VT_UI4:
case VarEnum.VT_UINT:
return ulVal;
case VarEnum.VT_I8:
return hVal;
case VarEnum.VT_UI8:
return uhVal;
case VarEnum.VT_R4:
return fltVal;
case VarEnum.VT_R8:
return dblVal;
case VarEnum.VT_BOOL:
return boolVal;
case VarEnum.VT_ERROR:
return scode;
case VarEnum.VT_CY:
return cyVal;
case VarEnum.VT_DATE:
return date;
case VarEnum.VT_FILETIME:
if (hVal > 0)
{
return DateTime.FromFileTime(hVal);
}
else
{
return null;
}
case VarEnum.VT_BSTR:
return Marshal.PtrToStringBSTR(p);
case VarEnum.VT_LPSTR:
return Marshal.PtrToStringAnsi(p);
case VarEnum.VT_LPWSTR:
return Marshal.PtrToStringUni(p);
case VarEnum.VT_UNKNOWN:
return Marshal.GetObjectForIUnknown(p);
case VarEnum.VT_DISPATCH:
return p;
case VarEnum.VT_CLSID:
return Marshal.PtrToStructure(p, typeof(Guid));
//default:
// throw new NotSupportedException("The type of this variable is not support ('" + vt.ToString() + "')");
}
return null;
}
}
public PROPVARIANT(string value)
{
this.vt = (ushort)VarEnum.VT_LPWSTR;
this.p = Marshal.StringToCoTaskMemUni(value);
this.p2 = 0;
this.wReserved1 = 0;
this.wReserved2 = 0;
this.wReserved3 = 0;
}
public PROPVARIANT(Guid value)
{
this.vt = (ushort)VarEnum.VT_CLSID;
byte[] guid = value.ToByteArray();
this.p = Marshal.AllocCoTaskMem(guid.Length);
Marshal.Copy(guid, 0, p, guid.Length);
this.p2 = 0;
this.wReserved1 = 0;
this.wReserved2 = 0;
this.wReserved3 = 0;
}
}
/// <summary>
/// This is the CoClass that impliments the shell link interfaces.
/// </summary>
[ComImport, Guid("00021401-0000-0000-C000-000000000046")]
internal class ShellLinkCoClass { }
/// <summary>The IShellLink interface allows Shell links to be created, modified, and resolved</summary>
[ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
interface IShellLinkW
{
/// <summary>Retrieves the path and file name of a Shell link object</summary>
void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out UnsafeNativeMethods.WIN32_FIND_DATAW pfd, UnsafeNativeMethods.SLGP_FLAGS fFlags);
/// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
void GetIDList(out IntPtr ppidl);
/// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
void SetIDList(IntPtr pidl);
/// <summary>Retrieves the description string for a Shell link object</summary>
void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
/// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
/// <summary>Retrieves the name of the working directory for a Shell link object</summary>
void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
/// <summary>Sets the name of the working directory for a Shell link object</summary>
void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
/// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
/// <summary>Sets the command-line arguments for a Shell link object</summary>
void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
/// <summary>Retrieves the hot key for a Shell link object</summary>
void GetHotkey(out short pwHotkey);
/// <summary>Sets a hot key for a Shell link object</summary>
void SetHotkey(short wHotkey);
/// <summary>Retrieves the show command for a Shell link object</summary>
void GetShowCmd(out int piShowCmd);
/// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
void SetShowCmd(int iShowCmd);
/// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
int cchIconPath, out int piIcon);
/// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
/// <summary>Sets the relative path to the Shell link object</summary>
void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
/// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
void Resolve(IntPtr hwnd, UnsafeNativeMethods.SLR_FLAGS fFlags);
/// <summary>Sets the path and file name of a Shell link object</summary>
void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport, InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("0000010B-0000-0000-C000-000000000046")]
internal interface IPersistFile
{
#region Methods inherited from IPersist
void GetClassID(out Guid pClassID);
#endregion
[PreserveSig]
int IsDirty();
void Load(
[MarshalAs(UnmanagedType.LPWStr)] string pszFileName,
int dwMode);
void Save(
[MarshalAs(UnmanagedType.LPWStr)] string pszFileName,
[MarshalAs(UnmanagedType.Bool)] bool fRemember);
void SaveCompleted(
[MarshalAs(UnmanagedType.LPWStr)] string pszFileName);
void GetCurFile(
out IntPtr ppszFileName);
}
[ComImport, Guid("886D8EEB-8CF2-4446-8D02-CDBA1DBDCF99"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IPropertyStore
{
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetCount([Out] out uint cProps);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetAt([In] uint iProp, out PropertyKey pkey);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void GetValue([In] ref PropertyKey key, out PROPVARIANT pv);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void SetValue([In] ref PropertyKey key, [In] ref PROPVARIANT pv);
[MethodImpl(MethodImplOptions.InternalCall, MethodCodeType = MethodCodeType.Runtime)]
void Commit();
}
class ShortcutManager
{
/// <summary>
/// Creates a shortcut to enable the app to receive toast notifications.
/// </summary>
/// <remarks>
/// Documentation: https://docs.microsoft.com/en-us/windows/win32/shell/enable-desktop-toast-with-appusermodelid
/// </remarks>
/// <param name="shortcutPath">Full path to the shortcut (including .lnk), must be in Program Files or Start Menu</param>
/// <param name="appExecutablePath">Path the to app that receives notifications</param>
/// <param name="arguments">Optional arguments</param>
/// <param name="appName">The name of the app - used to create the toast</param>
/// <param name="activatorId">The activation id</param>
public static void RegisterAppForNotifications(string shortcutPath, string appExecutablePath, string arguments, string appName, string activatorId)
{
var shellLinkClass = new ShellLinkCoClass();
IShellLinkW shellLink = (IShellLinkW)shellLinkClass;
shellLink.SetPath(appExecutablePath);
IPropertyStore propertyStore = (IPropertyStore)shellLinkClass;
IPersistFile persistFile = (IPersistFile)shellLinkClass;
if (arguments != null)
{
shellLink.SetArguments(arguments);
}
// https://docs.microsoft.com/en-us/windows/win32/properties/props-system-appusermodel-id
propertyStore.SetValue(new PropertyKey("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3", 5), new PROPVARIANT(appName));
// https://docs.microsoft.com/en-us/windows/win32/properties/props-system-appusermodel-toastactivatorclsid
propertyStore.SetValue(new PropertyKey("9F4C2855-9F79-4B39-A8D0-E1D42DE1D5F3", 26), new PROPVARIANT(new Guid(activatorId)));
propertyStore.Commit();
persistFile.Save(shortcutPath, true);
}
}
}
@derrickberg-dev
Copy link

Why do you say that you need the file https://raw.githubusercontent.com/WindowsNotifications/desktop-toasts/master/CS/DesktopToastsApp/DesktopNotificationManagerCompat.cs but I don't see any references to it. Looks like everything you needed came from using Microsoft.Toolkit.Uwp.Notifications

@davidair
Copy link
Author

Hey Derrick, I think you are right - I am guessing the newer versions of Microsoft.Toolkit.Uwp.Notifications added DesktopNotificationManagerCompat to the library so you no longer need to download the additional dependency.

@derrickberg-dev
Copy link

Hey Derrick, I think you are right - I am guessing the newer versions of Microsoft.Toolkit.Uwp.Notifications added DesktopNotificationManagerCompat to the library so you no longer need to download the additional dependency.

Have you tried creating scheduled toast notifications without ever calling the .show() method? I believe I found a bug, because I can’t pop up a notification using AddToSchedule unless I have used the show method before. I only learned of this after testing my app on other machines, couldn’t get notifications to work, created another version that just tested popping up a notification with .show(), which worked, then trying it again it would worked as expected. Very confusing!

@davidair
Copy link
Author

I never tried the scheduler - all toasts I did were shown immediately.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment