Skip to content

Instantly share code, notes, and snippets.

@davidair
Last active August 12, 2022 06:39
Show Gist options
  • 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);
}
}
}
@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