From 6fcbb11b464ad8222fb3563adbf7a87d079ab52a Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Tue, 18 Aug 2020 17:28:44 -0700 Subject: [PATCH 01/46] New Notifications code --- .../DesktopNotificationActivatedEventArgs.cs | 24 ++ .../DesktopNotificationManagerCompat.cs | 262 ++++++++++++++---- .../NotificationActivator.cs | 12 +- .../NotificationUserInput.cs | 6 +- .../DesktopNotificationManager/OnActivated.cs | 8 + ...Microsoft.Toolkit.Uwp.Notifications.csproj | 5 +- .../App.xaml | 8 + .../App.xaml.cs | 39 +++ .../AssemblyInfo.cs | 10 + .../MainWindow.xaml | 20 ++ .../MainWindow.xaml.cs | 28 ++ ...oft.Toolkit.Win32.WpfCore.SampleApp.csproj | 13 + Windows Community Toolkit.sln | 33 +++ 13 files changed, 406 insertions(+), 62 deletions(-) create mode 100644 Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationActivatedEventArgs.cs create mode 100644 Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/OnActivated.cs create mode 100644 Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml create mode 100644 Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs create mode 100644 Microsoft.Toolkit.Win32.WpfCore.SampleApp/AssemblyInfo.cs create mode 100644 Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml create mode 100644 Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs create mode 100644 Microsoft.Toolkit.Win32.WpfCore.SampleApp/Microsoft.Toolkit.Win32.WpfCore.SampleApp.csproj diff --git a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationActivatedEventArgs.cs b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationActivatedEventArgs.cs new file mode 100644 index 00000000000..a42329f0b46 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationActivatedEventArgs.cs @@ -0,0 +1,24 @@ +using Windows.Foundation.Collections; + +namespace Microsoft.Toolkit.Uwp.Notifications +{ + /// + /// Provides information about an event that occurs when the app is activated because a user tapped on the body of a toast notification or performed an action inside a toast notification. + /// + public class DesktopNotificationActivatedEventArgs + { + internal DesktopNotificationActivatedEventArgs() + { + } + + /// + /// Gets the arguments from the toast XML payload related to the action that was invoked on the toast. + /// + public string Argument { get; internal set; } + + /// + /// Gets a set of values that you can use to obtain the user input from an interactive toast notification. + /// + public ValueSet UserInput { get; internal set; } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationManagerCompat.cs b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationManagerCompat.cs index f70be4537fb..a9afbbcc3a1 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationManagerCompat.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/DesktopNotificationManagerCompat.cs @@ -4,10 +4,16 @@ using System; using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; using System.Runtime.InteropServices; +using System.Security.Cryptography; using System.Text; +using Microsoft.Win32; +using Windows.Foundation.Collections; +using Windows.UI; using Windows.UI.Notifications; -using static Microsoft.Toolkit.Uwp.Notifications.NotificationActivator; namespace Microsoft.Toolkit.Uwp.Notifications { @@ -16,10 +22,40 @@ namespace Microsoft.Toolkit.Uwp.Notifications /// public class DesktopNotificationManagerCompat { + /// + /// Event that is triggered when a notification or notification button is clicked. + /// + public static event OnActivated OnActivated; + + internal static void OnActivatedInternal(string args, Internal.NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] input, string aumid) + { + ValueSet userInput = new ValueSet(); + + if (input != null) + { + foreach (var val in input) + { + userInput.Add(val.Key, val.Value); + } + } + + try + { + OnActivated?.Invoke(new DesktopNotificationActivatedEventArgs() + { + Argument = args, + UserInput = userInput + }); + } + catch + { + } + } + /// /// A constant that is used as the launch arg when your EXE is launched from a toast notification. /// - public const string ToastActivatedLaunchArg = "-ToastActivated"; + private const string TOAST_ACTIVATED_LAUNCH_ARG = "-ToastActivated"; private const int CLASS_E_NOAGGREGATION = -2147221232; private const int E_NOINTERFACE = -2147467262; @@ -30,23 +66,57 @@ public class DesktopNotificationManagerCompat private static bool _registeredAumidAndComServer; private static string _aumid; - private static bool _registeredActivator; /// - /// If you're not using MSIX or sparse packages, you must call this method to register your AUMID with the Compat library and to - /// register your COM CLSID and EXE in LocalServer32 registry. Feel free to call this regardless, and we will no-op if running - /// under Desktop Bridge. Call this upon application startup, before calling any other APIs. + /// If you're not using UWP, MSIX, or sparse packages, you must call this method to register your AUMID + /// and display assets with the Compat library. Feel free to call this regardless, it will no-op if running + /// under MSIX/sparse/UWP. Call this upon application startup (every time), before calling any other APIs. + /// Note that the display name and icon will NOT update if changed until either all toasts are cleared, + /// or the system is rebooted. + /// + /// An AUMID that uniquely identifies your application. + /// Your app's display name, which will appear on toasts and within Action Center. + /// Your app's icon, which will appear on toasts and within Action Center. + public static void RegisterApplication( + string aumid, + string displayName, + string iconPath) + { + RegisterApplication(aumid, displayName, iconPath, Colors.LightGray); + } + + /// + /// If you're not using UWP, MSIX, or sparse packages, you must call this method to register your AUMID + /// and display assets with the Compat library. Feel free to call this regardless, it will no-op if running + /// under MSIX/sparse/UWP. Call this upon application startup (every time), before calling any other APIs. + /// Note that the display name and icon will NOT update if changed until either all toasts are cleared, + /// or the system is rebooted. /// - /// Your implementation of NotificationActivator. Must have GUID and ComVisible attributes on class. /// An AUMID that uniquely identifies your application. - public static void RegisterAumidAndComServer(string aumid) - where T : NotificationActivator + /// Your app's display name, which will appear on toasts and within Action Center. + /// Your app's icon, which will appear on toasts and within Action Center. + /// Your app's icon background color, which only appears in the system notification settings page (everywhere else your icon will have a transparent background). If you use the method without this parameter, we'll use light gray (which should look good on most icons). To use the accent color, pass in null for this parameter. + public static void RegisterApplication( + string aumid, + string displayName, + string iconPath, + Color iconBackgroundColor) { if (string.IsNullOrWhiteSpace(aumid)) { throw new ArgumentException("You must provide an AUMID.", nameof(aumid)); } + if (string.IsNullOrWhiteSpace(displayName)) + { + throw new ArgumentException("You must provide a display name.", nameof(displayName)); + } + + if (string.IsNullOrWhiteSpace(iconPath)) + { + throw new ArgumentException("You must provide an icon path.", nameof(iconPath)); + } + // If running as Desktop Bridge if (DesktopBridgeHelpers.IsRunningAsUwp()) { @@ -60,46 +130,145 @@ public static void RegisterAumidAndComServer(string aumid) _aumid = aumid; - string exePath = Process.GetCurrentProcess().MainModule.FileName; - RegisterComServer(exePath); + using (var rootKey = Registry.CurrentUser.CreateSubKey(@"Software\Classes\AppUserModelId\" + aumid)) + { + rootKey.SetValue("DisplayName", displayName); + rootKey.SetValue("IconUri", iconPath); + + // Background color only appears in the settings page, format is + // hex without leading #, like "FFDDDDDD" + if (iconBackgroundColor == null) + { + rootKey.DeleteValue("IconBackgroundColor"); + } + else + { + rootKey.SetValue("IconBackgroundColor", $"{iconBackgroundColor.A:X2}{iconBackgroundColor.R:X2}{iconBackgroundColor.G:X2}{iconBackgroundColor.B:X2}"); + } + } _registeredAumidAndComServer = true; + + // https://stackoverflow.com/questions/24069352/c-sharp-typebuilder-generate-class-with-function-dynamically + // For .NET Core we're going to need https://stackoverflow.com/questions/36937276/is-there-any-replace-of-assemblybuilder-definedynamicassembly-in-net-core + AssemblyName aName = new AssemblyName("DynamicComActivator"); + AssemblyBuilder aBuilder = AssemblyBuilder.DefineDynamicAssembly(aName, AssemblyBuilderAccess.Run); + + // For a single-module assembly, the module name is usually the assembly name plus an extension. + ModuleBuilder mb = aBuilder.DefineDynamicModule(aName.Name); + + // Create class which extends NotificationActivator + TypeBuilder tb = mb.DefineType( + name: "MyNotificationActivator", + attr: TypeAttributes.Public, + parent: typeof(Internal.NotificationActivator), + interfaces: new Type[0]); + + tb.SetCustomAttribute(new CustomAttributeBuilder( + con: typeof(GuidAttribute).GetConstructor(new Type[] { typeof(string) }), + constructorArgs: new object[] { GenerateGuid(aumid) })); + + tb.SetCustomAttribute(new CustomAttributeBuilder( + con: typeof(ComVisibleAttribute).GetConstructor(new Type[] { typeof(bool) }), + constructorArgs: new object[] { true })); + + tb.SetCustomAttribute(new CustomAttributeBuilder( +#pragma warning disable CS0618 // Type or member is obsolete + con: typeof(ComSourceInterfacesAttribute).GetConstructor(new Type[] { typeof(Type) }), +#pragma warning restore CS0618 // Type or member is obsolete + constructorArgs: new object[] { typeof(Internal.NotificationActivator.INotificationActivationCallback) })); + + tb.SetCustomAttribute(new CustomAttributeBuilder( + con: typeof(ClassInterfaceAttribute).GetConstructor(new Type[] { typeof(ClassInterfaceType) }), + constructorArgs: new object[] { ClassInterfaceType.None })); + + var activatorType = tb.CreateType(); + + RegisterActivator(activatorType); } - private static void RegisterComServer(string exePath) - where T : NotificationActivator + /// + /// From https://stackoverflow.com/a/41622689/1454643 + /// Generates Guid based on String. Key assumption for this algorithm is that name is unique (across where it it's being used) + /// and if name byte length is less than 16 - it will be fetched directly into guid, if over 16 bytes - then we compute sha-1 + /// hash from string and then pass it to guid. + /// + /// Unique name which is unique across where this guid will be used. + /// For example "706C7567-696E-7300-0000-000000000000" for "plugins" + private static string GenerateGuid(string name) { - // We register the EXE to start up when the notification is activated - string regString = string.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}\\LocalServer32", typeof(T).GUID); - var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(regString); + byte[] buf = Encoding.UTF8.GetBytes(name); + byte[] guid = new byte[16]; + if (buf.Length < 16) + { + Array.Copy(buf, guid, buf.Length); + } + else + { + using (SHA1 sha1 = SHA1.Create()) + { + byte[] hash = sha1.ComputeHash(buf); - // Include a flag so we know this was a toast activation and should wait for COM to process - // We also wrap EXE path in quotes for extra security - key.SetValue(null, '"' + exePath + '"' + " " + ToastActivatedLaunchArg); - } + // Hash is 20 bytes, but we need 16. We loose some of "uniqueness", but I doubt it will be fatal + Array.Copy(hash, guid, 16); + } + } - /* - * RegisterActivator code and all internal dependencies is from FrecherxDachs. - * See entry #2 in ThirdPartyNotices.txt in root repository directory for full license. */ + // Don't use Guid constructor, it tends to swap bytes. We want to preserve original string as hex dump. + string guidS = $"{guid[0]:X2}{guid[1]:X2}{guid[2]:X2}{guid[3]:X2}-{guid[4]:X2}{guid[5]:X2}-{guid[6]:X2}{guid[7]:X2}-{guid[8]:X2}{guid[9]:X2}-{guid[10]:X2}{guid[11]:X2}{guid[12]:X2}{guid[13]:X2}{guid[14]:X2}{guid[15]:X2}"; + + return guidS; + } /// - /// Registers the activator type as a COM server client so that Windows can launch your activator. + /// Registers the activator type as a COM server client so that Windows can launch your activator. If not using UWP/MSIX/sparse, you must call first. /// - /// Your implementation of NotificationActivator. Must have GUID and ComVisible attributes on class. - public static void RegisterActivator() - where T : NotificationActivator, new() + private static void RegisterActivator(Type activatorType) { + if (!DesktopBridgeHelpers.IsRunningAsUwp()) + { + if (_aumid == null) + { + throw new InvalidOperationException("You must call RegisterApplication first."); + } + + string exePath = Process.GetCurrentProcess().MainModule.FileName; + RegisterComServer(activatorType, exePath); + + using (var rootKey = Registry.CurrentUser.CreateSubKey(@"Software\Classes\AppUserModelId\" + _aumid)) + { + rootKey.SetValue("CustomActivator", string.Format("{{{0}}}", activatorType.GUID)); + } + } + // Big thanks to FrecherxDachs for figuring out the following code which works in .NET Core 3: https://github.com/FrecherxDachs/UwpNotificationNetCoreTest - var uuid = typeof(T).GUID; - uint cookie; + var uuid = activatorType.GUID; CoRegisterClassObject( uuid, - new NotificationActivatorClassFactory(), + new NotificationActivatorClassFactory(activatorType), CLSCTX_LOCAL_SERVER, REGCLS_MULTIPLEUSE, - out cookie); + out _); + } + + private static void RegisterComServer(Type activatorType, string exePath) + { + // We register the EXE to start up when the notification is activated + string regString = string.Format("SOFTWARE\\Classes\\CLSID\\{{{0}}}\\LocalServer32", activatorType.GUID); + var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(regString); + + // Include a flag so we know this was a toast activation and should wait for COM to process + // We also wrap EXE path in quotes for extra security + key.SetValue(null, '"' + exePath + '"' + " " + TOAST_ACTIVATED_LAUNCH_ARG); + } - _registeredActivator = true; + /// + /// Gets whether the current process was activated due to a toast activation. If so, the OnActivated event will be triggered soon after process launch. + /// + /// True if the current process was activated due to a toast activation, otherwise false. + public static bool WasProcessToastActivated() + { + return Environment.GetCommandLineArgs().Contains(TOAST_ACTIVATED_LAUNCH_ARG); } [ComImport] @@ -114,9 +283,15 @@ private interface IClassFactory int LockServer(bool fLock); } - private class NotificationActivatorClassFactory : IClassFactory - where T : NotificationActivator, new() + private class NotificationActivatorClassFactory : IClassFactory { + private Type _activatorType; + + public NotificationActivatorClassFactory(Type activatorType) + { + _activatorType = activatorType; + } + public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject) { ppvObject = IntPtr.Zero; @@ -126,12 +301,12 @@ public int CreateInstance(IntPtr pUnkOuter, ref Guid riid, out IntPtr ppvObject) Marshal.ThrowExceptionForHR(CLASS_E_NOAGGREGATION); } - if (riid == typeof(T).GUID || riid == IUnknownGuid) + if (riid == _activatorType.GUID || riid == IUnknownGuid) { // Create the instance of the .NET object ppvObject = Marshal.GetComInterfaceForObject( - new T(), - typeof(INotificationActivationCallback)); + Activator.CreateInstance(_activatorType), + typeof(Internal.NotificationActivator.INotificationActivationCallback)); } else { @@ -158,7 +333,7 @@ private static extern int CoRegisterClassObject( out uint lpdwRegister); /// - /// Creates a toast notifier. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// Creates a toast notifier. If you're a Win32 non-MSIX/sparse app, you must have called first), or this will throw an exception. /// /// public static ToastNotifier CreateToastNotifier() @@ -178,7 +353,7 @@ public static ToastNotifier CreateToastNotifier() } /// - /// Gets the object. You must have called first (and also if you're a classic Win32 app), or this will throw an exception. + /// Gets the object. If you're a Win32 non-MISX/sparse app, you must call first, or this will throw an exception. /// public static DesktopNotificationHistoryCompat History { @@ -204,16 +379,9 @@ private static void EnsureRegistered() else { // Otherwise, incorrect usage - throw new Exception("You must call RegisterAumidAndComServer first."); + throw new Exception($"You must call {nameof(RegisterApplication)} first."); } } - - // If not registered activator yet - if (!_registeredActivator) - { - // Incorrect usage - throw new Exception("You must call RegisterActivator first."); - } } /// diff --git a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationActivator.cs b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationActivator.cs index b8cf3c8ae27..0946b2be610 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationActivator.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationActivator.cs @@ -11,7 +11,7 @@ using System.Text; using Windows.UI.Notifications; -namespace Microsoft.Toolkit.Uwp.Notifications +namespace Microsoft.Toolkit.Uwp.Notifications.Internal { /// /// Apps must implement this activator to handle notification activation. @@ -21,17 +21,9 @@ public abstract class NotificationActivator : NotificationActivator.INotificatio /// public void Activate(string appUserModelId, string invokedArgs, NOTIFICATION_USER_INPUT_DATA[] data, uint dataCount) { - OnActivated(invokedArgs, new NotificationUserInput(data), appUserModelId); + DesktopNotificationManagerCompat.OnActivatedInternal(invokedArgs, data, appUserModelId); } - /// - /// This method will be called when the user clicks on a foreground or background activation on a toast. Parent app must implement this method. - /// - /// The arguments from the original notification. This is either the launch argument if the user clicked the body of your toast, or the arguments from a button on your toast. - /// Text and selection values that the user entered in your toast. - /// Your AUMID. - public abstract void OnActivated(string arguments, NotificationUserInput userInput, string appUserModelId); - /// /// A single user input key/value pair. /// diff --git a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationUserInput.cs b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationUserInput.cs index 0ef54e3ec39..25f7b6b3e0f 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationUserInput.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/NotificationUserInput.cs @@ -16,11 +16,11 @@ namespace Microsoft.Toolkit.Uwp.Notifications /// /// Text and selection values that the user entered on your notification. The Key is the ID of the input, and the Value is what the user entered. /// - public class NotificationUserInput : IReadOnlyDictionary + internal class NotificationUserInput : IReadOnlyDictionary { - private NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] _data; + private Internal.NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] _data; - internal NotificationUserInput(NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] data) + internal NotificationUserInput(Internal.NotificationActivator.NOTIFICATION_USER_INPUT_DATA[] data) { _data = data; } diff --git a/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/OnActivated.cs b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/OnActivated.cs new file mode 100644 index 00000000000..0b2142ca2e2 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Notifications/DesktopNotificationManager/OnActivated.cs @@ -0,0 +1,8 @@ +namespace Microsoft.Toolkit.Uwp.Notifications +{ + /// + /// Event triggered when a notification is clicked. + /// + /// Arguments that specify what action was taken and the user inputs. + public delegate void OnActivated(DesktopNotificationActivatedEventArgs e); +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj b/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj index 1c08ba0f600..3e72883b2ff 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj +++ b/Microsoft.Toolkit.Uwp.Notifications/Microsoft.Toolkit.Uwp.Notifications.csproj @@ -1,7 +1,7 @@  - netstandard1.4;uap10.0;native;net461;netcoreapp3.1 + netstandard1.4;uap10.0;native;net461;netcoreapp3.0 $(DefineConstants);NETFX_CORE Windows Community Toolkit Notifications @@ -39,9 +39,10 @@ - + + diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml new file mode 100644 index 00000000000..8cde026f920 --- /dev/null +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml @@ -0,0 +1,8 @@ + + + + + diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs new file mode 100644 index 00000000000..1683e82d10e --- /dev/null +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs @@ -0,0 +1,39 @@ +using System; +using System.Windows; +using Microsoft.Toolkit.Uwp.Notifications; + +namespace Microsoft.Toolkit.Win32.WpfCore.SampleApp +{ + /// + /// Interaction logic for App.xaml + /// + public partial class App : Application + { + protected override void OnStartup(StartupEventArgs e) + { + // Register the app to support toast notifications + DesktopNotificationManagerCompat.RegisterApplication( + aumid: "Microsoft.Toolkit.Win32.WpfCore", + displayName: "Toolkit Win32 WPF Core Sample App", + iconPath: "C:\\icon.png"); + + // And listen to toast notification activations + DesktopNotificationManagerCompat.OnActivated += this.DesktopNotificationManagerCompat_OnActivated; + + if (!DesktopNotificationManagerCompat.WasProcessToastActivated()) + { + new MainWindow().Show(); + } + } + + private void DesktopNotificationManagerCompat_OnActivated(DesktopNotificationActivatedEventArgs e) + { + Dispatcher.Invoke(() => + { + var args = e.Argument; + var userInputCount = e.UserInput.Count; + MessageBox.Show("Activated!"); + }); + } + } +} diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/AssemblyInfo.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/AssemblyInfo.cs new file mode 100644 index 00000000000..8b5504ecfbb --- /dev/null +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/AssemblyInfo.cs @@ -0,0 +1,10 @@ +using System.Windows; + +[assembly: ThemeInfo( + ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located + //(used if a resource is not found in the page, + // or application resource dictionaries) + ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located + //(used if a resource is not found in the page, + // app, or any theme specific resource dictionaries) +)] diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml new file mode 100644 index 00000000000..a86aa2968f1 --- /dev/null +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml @@ -0,0 +1,20 @@ + + + + + + + + /// The key. - /// The optional value of the parameter. - public void Set(string key, string value) + /// The optional value of the key. + /// The current object. + public ToastArguments Set(string key, string value) { if (key == null) { @@ -128,6 +227,75 @@ public void Set(string key, string value) } _dictionary[key] = value; + + return this; + } + + /// + /// Sets a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. + public ToastArguments Set(string key, int value) + { + return SetHelper(key, value); + } + + /// + /// Sets a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. + public ToastArguments Set(string key, double value) + { + return SetHelper(key, value); + } + + /// + /// Sets a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. + public ToastArguments Set(string key, float value) + { + return SetHelper(key, value); + } + + /// + /// Sets a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. + /// The current object. + public ToastArguments Set(string key, bool value) + { + return Set(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space + } + + /// + /// Sets a key and value. If there is an existing key, it is replaced. + /// + /// The key. + /// The value of the key. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current object. + public ToastArguments Set(string key, Enum value) + { + return Set(key, (int)(object)value); + } + + private ToastArguments SetHelper(string key, object value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + _dictionary[key] = value.ToString(); + + return this; } /// diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs index ff744115567..6e587b53bf2 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs @@ -48,71 +48,74 @@ private void ToastNotificationManagerCompat_OnActivated(ToastNotificationActivat ToastArguments args = ToastArguments.Parse(e.Argument); // See what action is being requested - switch (args["action"]) + if (args.TryGetValue("action", out MyToastActions action)) { - // Open the image - case "viewImage": + switch (action) + { + // Open the image + case MyToastActions.ViewImage: - // The URL retrieved from the toast args - string imageUrl = args["imageUrl"]; + // The URL retrieved from the toast args + string imageUrl = args["imageUrl"]; - // Make sure we have a window open and in foreground - OpenWindowIfNeeded(); + // Make sure we have a window open and in foreground + OpenWindowIfNeeded(); - // And then show the image - (Current.Windows[0] as MainWindow).ShowImage(imageUrl); + // And then show the image + (Current.Windows[0] as MainWindow).ShowImage(imageUrl); - break; + break; - // Open the conversation - case "viewConversation": + // Open the conversation + case MyToastActions.ViewConversation: - // The conversation ID retrieved from the toast args - int conversationId = int.Parse(args["conversationId"]); + // The conversation ID retrieved from the toast args + int conversationId = args.GetInt("conversationId"); - // Make sure we have a window open and in foreground - OpenWindowIfNeeded(); + // Make sure we have a window open and in foreground + OpenWindowIfNeeded(); - // And then show the conversation - (Current.Windows[0] as MainWindow).ShowConversation(); + // And then show the conversation + (Current.Windows[0] as MainWindow).ShowConversation(); - break; + break; - // Background: Quick reply to the conversation - case "reply": + // Background: Quick reply to the conversation + case MyToastActions.Reply: - // Get the response the user typed - string msg = e.UserInput["tbReply"] as string; + // Get the response the user typed + string msg = e.UserInput["tbReply"] as string; - // And send this message - ShowToast("Message sent: " + msg); + // And send this message + ShowToast("Message sent: " + msg); - // If there's no windows open, exit the app - if (Current.Windows.Count == 0) - { - Current.Shutdown(); - } + // If there's no windows open, exit the app + if (Current.Windows.Count == 0) + { + Current.Shutdown(); + } - break; + break; - // Background: Send a like - case "like": + // Background: Send a like + case MyToastActions.Like: - ShowToast("Like sent!"); + ShowToast("Like sent!"); - // If there's no windows open, exit the app - if (Current.Windows.Count == 0) - { - Current.Shutdown(); - } + // If there's no windows open, exit the app + if (Current.Windows.Count == 0) + { + Current.Shutdown(); + } - break; + break; - default: + default: - OpenWindowIfNeeded(); + OpenWindowIfNeeded(); - break; + break; + } } }); } diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs index d04380548be..84cb107ffdb 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs @@ -37,10 +37,8 @@ private async void ButtonPopToast_Click(object sender, RoutedEventArgs e) // Arguments when the user taps body of toast .AddToastActivationInfo(new ToastArguments() - { - { "action", "viewConversation" }, - { "conversationId", conversationId.ToString() } - }) + .Set("action", MyToastActions.ViewConversation) + .Set("conversationId", conversationId)) // Visual content .AddText(title) @@ -53,22 +51,16 @@ private async void ButtonPopToast_Click(object sender, RoutedEventArgs e) // Buttons .AddButton("Reply", ToastActivationType.Background, new ToastArguments() - { - { "action", "reply" }, - { "conversationId", conversationId.ToString() } - }) + .Set("action", MyToastActions.Reply) + .Set("conversationId", conversationId)) .AddButton("Like", ToastActivationType.Background, new ToastArguments() - { - { "action", "like" }, - { "conversationId", conversationId.ToString() } - }) + .Set("action", MyToastActions.Like) + .Set("conversationId", conversationId)) .AddButton("View", ToastActivationType.Foreground, new ToastArguments() - { - { "action", "viewImage" }, - { "imageUrl", image } - }) + .Set("action", MyToastActions.ViewImage) + .Set("imageUrl", image)) // And show the toast! .Show(); diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs new file mode 100644 index 00000000000..dd93d2d1865 --- /dev/null +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs @@ -0,0 +1,29 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Toolkit.Win32.WpfCore.SampleApp +{ + public enum MyToastActions + { + /// + /// View the conversation + /// + ViewConversation, + + /// + /// Inline reply to a message + /// + Reply, + + /// + /// Like a message + /// + Like, + + /// + /// View the image included in the message + /// + ViewImage + } +} From 634478df8720e80cb7274cbf4de4bc01c3507cbd Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Fri, 30 Oct 2020 10:13:31 -0600 Subject: [PATCH 38/46] Tests and fixes for ToastArguments --- .../Toasts/ToastArguments.cs | 77 ++- .../TestToastArguments.cs | 506 ++++++++++++++++++ .../TestToastContentBuilder.cs | 88 +++ .../UnitTests.Notifications.Shared.projitems | 1 + 4 files changed, 652 insertions(+), 20 deletions(-) create mode 100644 UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs index 1013ef1e300..fb61aea283a 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs @@ -6,7 +6,7 @@ namespace Microsoft.Toolkit.Uwp.Notifications { /// - /// A portable string serializer/deserializer for .NET. + /// A class that supports serializing simple key/value pairs into a format that's friendly for being used within toast notifications. The serialized format is similar to a query string, however optimized for being placed within an XML property (uses semicolons instead of ampersands since those don't need to be XML-escaped, doesn't url-encode all special characters since not being used within a URL, etc). /// public sealed class ToastArguments : IEnumerable> { @@ -66,6 +66,7 @@ public bool TryGetValue(string key, out string value) return _dictionary.TryGetValue(key, out value); } +#if !WINRT /// /// Attempts to get the value of the specified key. If no key exists, returns false. /// @@ -73,9 +74,6 @@ public bool TryGetValue(string key, out string value) /// The key to find. /// The key's value will be written here if found. /// True if found the key and set the value, otherwise false. -#if WINRT - [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("found")] -#endif public bool TryGetValue(string key, out T value) where T : struct, Enum { @@ -87,6 +85,7 @@ public bool TryGetValue(string key, out T value) value = default(T); return false; } +#endif /// /// Gets the value of the specified key, or throws if key didn't exist. @@ -138,6 +137,16 @@ public float GetFloat(string key) return float.Parse(Get(key)); } + /// + /// Gets the value of the specified key, or throws if key didn't exist. + /// + /// The key to get. + /// The value of the key. + public byte GetByte(string key) + { + return byte.Parse(Get(key)); + } + /// /// Gets the value of the specified key, or throws if key didn't exist. /// @@ -148,6 +157,7 @@ public bool GetBool(string key) return Get(key) == "1" ? true : false; } +#if !WINRT /// /// Gets the value of the specified key, or throws if key didn't exist. /// @@ -164,6 +174,7 @@ public T GetEnum(string key) throw new KeyNotFoundException(); } +#endif /// /// Gets the number of key/value pairs contained in the toast arguments. @@ -203,7 +214,11 @@ public void Add(string key, string value) /// Sets a key. If there is an existing key, it is replaced. /// /// The key. - public void Set(string key) + /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif + public ToastArguments Set(string key) { if (key == null) { @@ -211,6 +226,8 @@ public void Set(string key) } _dictionary[key] = null; + + return this; } /// @@ -219,6 +236,10 @@ public void Set(string key) /// The key. /// The optional value of the key. /// The current object. +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif public ToastArguments Set(string key, string value) { if (key == null) @@ -237,6 +258,9 @@ public ToastArguments Set(string key, string value) /// The key. /// The value of the key. /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif public ToastArguments Set(string key, int value) { return SetHelper(key, value); @@ -248,6 +272,9 @@ public ToastArguments Set(string key, int value) /// The key. /// The value of the key. /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif public ToastArguments Set(string key, double value) { return SetHelper(key, value); @@ -259,6 +286,9 @@ public ToastArguments Set(string key, double value) /// The key. /// The value of the key. /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif public ToastArguments Set(string key, float value) { return SetHelper(key, value); @@ -270,11 +300,15 @@ public ToastArguments Set(string key, float value) /// The key. /// The value of the key. /// The current object. +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] +#endif public ToastArguments Set(string key, bool value) { return Set(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space } +#if !WINRT /// /// Sets a key and value. If there is an existing key, it is replaced. /// @@ -285,6 +319,7 @@ public ToastArguments Set(string key, Enum value) { return Set(key, (int)(object)value); } +#endif private ToastArguments SetHelper(string key, object value) { @@ -347,18 +382,20 @@ public bool Remove(string key) return _dictionary.Remove(key); } - private static string UrlEncode(string str) + private static string Encode(string str) { - return Uri.EscapeDataString(str) - - // It incorrectly encodes spaces as %20, should use + - .Replace("%20", "+"); + return str + .Replace("%", "%25") + .Replace(";", "%3B") + .Replace("=", "%3D"); } - private static string UrlDecode(string str) + private static string Decode(string str) { - // Doesn't handle decoding the +, so we manually do that - return Uri.UnescapeDataString(str.Replace('+', ' ')); + return str + .Replace("%25", "%") + .Replace("%3B", ";") + .Replace("%3D", "="); } /// @@ -373,7 +410,7 @@ public static ToastArguments Parse(string toastArgumentsStr) return new ToastArguments(); } - string[] pairs = toastArgumentsStr.Split('&'); + string[] pairs = toastArgumentsStr.Split(';'); ToastArguments answer = new ToastArguments(); @@ -386,13 +423,13 @@ public static ToastArguments Parse(string toastArgumentsStr) if (indexOfEquals == -1) { - name = UrlDecode(pair); + name = Decode(pair); value = null; } else { - name = UrlDecode(pair.Substring(0, indexOfEquals)); - value = UrlDecode(pair.Substring(indexOfEquals + 1)); + name = Decode(pair.Substring(0, indexOfEquals)); + value = Decode(pair.Substring(indexOfEquals + 1)); } answer.Add(name, value); @@ -407,13 +444,13 @@ public static ToastArguments Parse(string toastArgumentsStr) /// A string that can be used within a toast notification. public sealed override string ToString() { - return string.Join("&", this.Select(pair => + return string.Join(";", this.Select(pair => // Key - UrlEncode(pair.Key) + + Encode(pair.Key) + // Write value if not null - ((pair.Value == null) ? string.Empty : ("=" + UrlEncode(pair.Value))))); + ((pair.Value == null) ? string.Empty : ("=" + Encode(pair.Value))))); } /// diff --git a/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs b/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs new file mode 100644 index 00000000000..7f170cf3ada --- /dev/null +++ b/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs @@ -0,0 +1,506 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Toolkit.Uwp.Notifications; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace UnitTests.Notifications +{ + [TestClass] + public class TestToastArguments + { + [TestMethod] + public void TestAddExceptions_NullName() + { + ToastArguments query = new ToastArguments(); + + try + { + query.Add(null, "value"); + } + catch (ArgumentNullException) + { + return; + } + + Assert.Fail("Adding null name shouldn't be allowed."); + } + + [TestMethod] + public void TestParsing() + { + AssertParse(new ToastArguments(), string.Empty); + AssertParse(new ToastArguments(), " "); + AssertParse(new ToastArguments(), "\n"); + AssertParse(new ToastArguments(), "\t \n"); + AssertParse(new ToastArguments(), null); + + AssertParse( + new ToastArguments() + { + { "isBook" } + }, "isBook"); + + AssertParse( + new ToastArguments() + { + { "isBook" }, + { "isRead" } + }, "isBook;isRead"); + + AssertParse( + new ToastArguments() + { + { "isBook" }, + { "isRead" }, + { "isLiked" } + }, "isBook;isRead;isLiked"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" } + }, "name=Andrew"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" }, + { "isAdult" } + }, "name=Andrew;isAdult"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" }, + { "isAdult" } + }, "isAdult;name=Andrew"); + + AssertParse( + new ToastArguments() + { + { "name", "Andrew" }, + { "age", "22" } + }, "age=22;name=Andrew"); + } + + [TestMethod] + public void TestToString_ExactString() + { + Assert.AreEqual(string.Empty, new ToastArguments().ToString()); + + Assert.AreEqual("isBook", new ToastArguments() + { + { "isBook" } + }.ToString()); + + Assert.AreEqual("name=Andrew", new ToastArguments() + { + { "name", "Andrew" } + }.ToString()); + } + + [TestMethod] + public void TestSpecialCharacters() + { + Assert.AreEqual("full name=Andrew Leader", new ToastArguments() + { + { "full name", "Andrew Leader" } + }.ToString()); + + Assert.AreEqual("name%3Bcompany=Andrew%3BMicrosoft", new ToastArguments() + { + { "name;company", "Andrew;Microsoft" } + }.ToString()); + + Assert.AreEqual("name/company=Andrew/Microsoft", new ToastArguments() + { + { "name/company", "Andrew/Microsoft" } + }.ToString()); + + Assert.AreEqual("message=Dinner?", new ToastArguments() + { + { "message", "Dinner?" } + }.ToString()); + + Assert.AreEqual("message=to: Andrew", new ToastArguments() + { + { "message", "to: Andrew" } + }.ToString()); + + Assert.AreEqual("email=andrew@live.com", new ToastArguments() + { + { "email", "andrew@live.com" } + }.ToString()); + + Assert.AreEqual("messsage=food%3Dyummy", new ToastArguments() + { + { "messsage", "food=yummy" } + }.ToString()); + + Assert.AreEqual("messsage=$$$", new ToastArguments() + { + { "messsage", "$$$" } + }.ToString()); + + Assert.AreEqual("messsage=-_.!~*'()", new ToastArguments() + { + { "messsage", "-_.!~*'()" } + }.ToString()); + + Assert.AreEqual("messsage=this & that", new ToastArguments() + { + { "messsage", "this & that" } + }.ToString()); + + Assert.AreEqual("messsage=20%25 off!", new ToastArguments() + { + { "messsage", "20% off!" } + }.ToString()); + + Assert.AreEqual("messsage=Nonsense %2526 %2525 %253D", new ToastArguments() + { + { "messsage", "Nonsense %26 %25 %3D" } + }.ToString()); + } + + [TestMethod] + public void TestDecoding() + { + AssertDecode("Hello world", "Hello world"); + + AssertDecode(";/?:@&=+$%", "%3B/?:@&%3D+$%25"); + AssertDecode("-_.!~*'()", "-_.!~*'()"); + } + +#if !WINRT + [TestMethod] + public void TestIndexer_NullException() + { + try + { + string val = new ToastArguments()[null]; + } + catch (ArgumentNullException) + { + return; + } + + Assert.Fail("Exception should have been thrown."); + } + + [TestMethod] + public void TestIndexer_NotFoundException() + { + try + { + var args = new ToastArguments() + { + { "name", "Andrew" } + }; + + _ = args["Name"]; + } + catch (KeyNotFoundException) + { + return; + } + + Assert.Fail("Exception should have been thrown."); + } + + [TestMethod] + public void TestIndexer() + { + AssertIndexer(null, "isBook;name=Andrew", "isBook"); + + AssertIndexer("Andrew", "isBook;name=Andrew", "name"); + + AssertIndexer("Andrew", "count=2;name=Andrew", "name"); + } +#endif + + [TestMethod] + public void TestRemove_OnlyKey() + { + ToastArguments qs = new ToastArguments() + { + { "name", "Andrew" }, + { "age", "22" } + }; + + Assert.IsTrue(qs.Remove("age")); + + AssertEqual( + new ToastArguments() + { + { "name", "Andrew" } + }, qs); + + Assert.IsFalse(qs.Remove("age")); + } + + [TestMethod] + public void TestContains() + { + ToastArguments qs = new ToastArguments(); + + Assert.IsFalse(qs.Contains("name")); + Assert.IsFalse(qs.Contains("name", "Andrew")); + + qs.Add("isBook"); + + Assert.IsFalse(qs.Contains("name")); + Assert.IsFalse(qs.Contains("name", "Andrew")); + + Assert.IsTrue(qs.Contains("isBook")); + Assert.IsTrue(qs.Contains("isBook", null)); + Assert.IsFalse(qs.Contains("isBook", "True")); + + qs.Set("isBook", "True"); + + Assert.IsTrue(qs.Contains("isBook")); + Assert.IsFalse(qs.Contains("isBook", null)); + Assert.IsTrue(qs.Contains("isBook", "True")); + + qs.Add("name", "Andrew"); + + Assert.IsTrue(qs.Contains("name")); + Assert.IsFalse(qs.Contains("name", null)); // Value doesn't exist + Assert.IsTrue(qs.Contains("name", "Andrew")); + Assert.IsFalse(qs.Contains("Name", "Andrew")); // Wrong case on name + Assert.IsFalse(qs.Contains("name", "andrew")); // Wrong case on value + Assert.IsFalse(qs.Contains("Name")); // Wrong case on name + } + + [TestMethod] + public void TestSet() + { + ToastArguments qs = new ToastArguments(); + + qs.Set("name", "Andrew"); + + AssertEqual( + new ToastArguments() + { + { "name", "Andrew" } + }, qs); + + qs.Set("age", "22"); + + AssertEqual( + new ToastArguments() + { + { "name", "Andrew" }, + { "age", "22" } + }, qs); + + qs.Set("name", "Lei"); + + AssertEqual( + new ToastArguments() + { + { "name", "Lei" }, + { "age", "22" } + }, qs); + + string nullStr = null; + qs.Set("name", nullStr); + + AssertEqual( + new ToastArguments() + { + { "name" }, + { "age", "22" } + }, qs); + } + + [TestMethod] + public void TestEnumerator() + { + KeyValuePair[] parameters = ToastArguments.Parse("name=Andrew;age=22;isOld").ToArray(); + + Assert.AreEqual(3, parameters.Length); + Assert.AreEqual(1, parameters.Count(i => i.Key.Equals("name"))); + Assert.AreEqual(1, parameters.Count(i => i.Key.Equals("age"))); + Assert.AreEqual(1, parameters.Count(i => i.Key.Equals("isOld"))); + Assert.IsTrue(parameters.Any(i => i.Key.Equals("name") && i.Value.Equals("Andrew"))); + Assert.IsTrue(parameters.Any(i => i.Key.Equals("age") && i.Value.Equals("22"))); + Assert.IsTrue(parameters.Any(i => i.Key.Equals("isOld") && i.Value == null)); + } + + [TestMethod] + public void TestCount() + { + ToastArguments qs = new ToastArguments(); + + Assert.AreEqual(0, qs.Count); + + qs.Add("name", "Andrew"); + + Assert.AreEqual(1, qs.Count); + + qs.Add("age", "22"); + + Assert.AreEqual(2, qs.Count); + + qs.Remove("age"); + + Assert.AreEqual(1, qs.Count); + + qs.Remove("name"); + + Assert.AreEqual(0, qs.Count); + } + + [TestMethod] + public void TestStronglyTyped() + { + ToastArguments args = new ToastArguments() + .Set("isAdult", true) + .Set("isPremium", false) + .Set("age", 22) + .Set("level", 0) + .Set("gpa", 3.97) + .Set("percent", 97.3f); + +#if !WINRT + args.Set("activationKind", ToastActivationType.Background); +#endif + + AssertEqual( + new ToastArguments() + { + { "isAdult", "1" }, + { "isPremium", "0" }, + { "age", "22" }, + { "level", "0" }, + { "gpa", "3.97" }, + { "percent", "97.3" }, +#if !WINRT + { "activationKind", "1" } +#endif + }, args); + + Assert.AreEqual(true, args.GetBool("isAdult")); + Assert.AreEqual("1", args.Get("isAdult")); + + Assert.AreEqual(false, args.GetBool("isPremium")); + Assert.AreEqual("0", args.Get("isPremium")); + + Assert.AreEqual(22, args.GetInt("age")); + Assert.AreEqual(22d, args.GetDouble("age")); + Assert.AreEqual(22f, args.GetFloat("age")); + Assert.AreEqual("22", args.Get("age")); + + Assert.AreEqual(0, args.GetInt("level")); + + Assert.AreEqual(3.97d, args.GetDouble("gpa")); + + Assert.AreEqual(97.3f, args.GetFloat("percent")); + +#if !WINRT + Assert.AreEqual(ToastActivationType.Background, args.GetEnum("activationKind")); + + if (args.TryGetValue("activationKind", out ToastActivationType activationType)) + { + Assert.AreEqual(ToastActivationType.Background, activationType); + } + else + { + Assert.Fail("TryGetValue as enum failed"); + } + + // Trying to get enum that isn't an enum should return false + Assert.IsFalse(args.TryGetValue("percent", out activationType)); +#endif + + // After serializing and deserializing, the same should work + args = ToastArguments.Parse(args.ToString()); + + Assert.AreEqual(true, args.GetBool("isAdult")); + Assert.AreEqual("1", args.Get("isAdult")); + + Assert.AreEqual(false, args.GetBool("isPremium")); + Assert.AreEqual("0", args.Get("isPremium")); + + Assert.AreEqual(22, args.GetInt("age")); + Assert.AreEqual(22d, args.GetDouble("age")); + Assert.AreEqual(22f, args.GetFloat("age")); + Assert.AreEqual("22", args.Get("age")); + + Assert.AreEqual(0, args.GetInt("level")); + + Assert.AreEqual(3.97d, args.GetDouble("gpa")); + + Assert.AreEqual(97.3f, args.GetFloat("percent")); + +#if !WINRT + Assert.AreEqual(ToastActivationType.Background, args.GetEnum("activationKind")); + + if (args.TryGetValue("activationKind", out activationType)) + { + Assert.AreEqual(ToastActivationType.Background, activationType); + } + else + { + Assert.Fail("TryGetValue as enum failed"); + } + + // Trying to get enum that isn't an enum should return false + Assert.IsFalse(args.TryGetValue("percent", out activationType)); +#endif + } + +#if !WINRT + private static void AssertIndexer(string expected, string queryString, string paramName) + { + ToastArguments q = ToastArguments.Parse(queryString); + + Assert.AreEqual(expected, q[paramName]); + } +#endif + + private static void AssertDecode(string expected, string encoded) + { + Assert.AreEqual(expected, ToastArguments.Parse("message=" + encoded).Get("message")); + } + + private static void AssertParse(ToastArguments expected, string inputQueryString) + { + Assert.IsTrue(IsSame(expected, ToastArguments.Parse(inputQueryString)), "Expected: " + expected + "\nActual: " + inputQueryString); + } + + private static void AssertEqual(ToastArguments expected, ToastArguments actual) + { + Assert.IsTrue(IsSame(expected, actual), "Expected: " + expected + "\nActual: " + actual); + + Assert.IsTrue(IsSame(expected, ToastArguments.Parse(actual.ToString())), "After serializing and parsing actual, result changed.\n\nExpected: " + expected + "\nActual: " + ToastArguments.Parse(actual.ToString())); + Assert.IsTrue(IsSame(ToastArguments.Parse(expected.ToString()), actual), "After serializing and parsing expected, result changed.\n\nExpected: " + ToastArguments.Parse(expected.ToString()) + "\nActual: " + actual); + Assert.IsTrue(IsSame(ToastArguments.Parse(expected.ToString()), ToastArguments.Parse(actual.ToString())), "After serializing and parsing both, result changed.\n\nExpected: " + ToastArguments.Parse(expected.ToString()) + "\nActual: " + ToastArguments.Parse(actual.ToString())); + } + + private static bool IsSame(ToastArguments expected, ToastArguments actual) + { + if (expected.Count != actual.Count) + { + return false; + } + + foreach (var pair in expected) + { + if (!actual.Contains(pair.Key)) + { + return false; + } + + if (actual.Get(pair.Key) != pair.Value) + { + return false; + } + } + + return true; + } + } +} \ No newline at end of file diff --git a/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs b/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs index 16c66aefe1c..89d55e4060f 100644 --- a/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs +++ b/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs @@ -80,6 +80,23 @@ public void AddToastActivationInfoDefaultTest_WithExpectedArgs_ReturnSelfWithAct Assert.AreEqual(ToastActivationType.Foreground, builder.Content.ActivationType); } + [TestMethod] + public void AddToastActivationInfoTest_WithExpectedArgs_UsingToastArguments() + { + // Arrange + ToastArguments args = new ToastArguments().Set("name", "Andrew"); + ToastActivationType testToastActivationType = ToastActivationType.Background; + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddToastActivationInfo(args, testToastActivationType); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(args.ToString(), builder.Content.Launch); + Assert.AreEqual(testToastActivationType, builder.Content.ActivationType); + } + [TestMethod] public void SetToastDurationTest_WithCustomToastDuration_ReturnSelfWithCustomToastDurationSet() { @@ -545,6 +562,27 @@ public void AddButtonTest_WithTextOnlyButton_ReturnSelfWithButtonAdded() Assert.AreEqual(testButtonLaunchArgs, button.Arguments); } + [TestMethod] + public void AddButtonTest_WithTextOnlyButtonAndToastArguments_ReturnSelfWithButtonAdded() + { + // Arrange + string testButtonContent = "Test Button Content"; + ToastActivationType testToastActivationType = ToastActivationType.Background; + var testButtonLaunchArgs = new ToastArguments().Set("action", "view"); + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddButton(testButtonContent, testToastActivationType, testButtonLaunchArgs); + + // Assert + Assert.AreSame(builder, anotherReference); + + var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; + Assert.AreEqual(testButtonContent, button.Content); + Assert.AreEqual(testToastActivationType, button.ActivationType); + Assert.AreEqual(testButtonLaunchArgs.ToString(), button.Arguments); + } + [TestMethod] public void AddButtonTest_WithCustomImageAndTextButton_ReturnSelfWithButtonAdded() { @@ -569,6 +607,30 @@ public void AddButtonTest_WithCustomImageAndTextButton_ReturnSelfWithButtonAdded Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); } + [TestMethod] + public void AddButtonTest_WithCustomImageAndTextButtonAndToastArguments_ReturnSelfWithButtonAdded() + { + // Arrange + string testButtonContent = "Test Button Content"; + ToastActivationType testToastActivationType = ToastActivationType.Background; + var testButtonLaunchArgs = new ToastArguments().Set("action", "accept"); + Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddButton(testButtonContent, testToastActivationType, testButtonLaunchArgs, testImageUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + + var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; + Assert.AreEqual(testButtonContent, button.Content); + Assert.AreEqual(testToastActivationType, button.ActivationType); + Assert.AreEqual(testButtonLaunchArgs.ToString(), button.Arguments); + Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); + } + [TestMethod] public void AddButtonTest_WithTextBoxId_ReturnSelfWithButtonAdded() { @@ -595,6 +657,32 @@ public void AddButtonTest_WithTextBoxId_ReturnSelfWithButtonAdded() Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); } + [TestMethod] + public void AddButtonTest_WithTextBoxIdAndToastArguments_ReturnSelfWithButtonAdded() + { + // Arrange + string testInputTextBoxId = Guid.NewGuid().ToString(); + string testButtonContent = "Test Button Content"; + ToastActivationType testToastActivationType = ToastActivationType.Background; + var testButtonLaunchArgs = new ToastArguments().Set("action", "send"); + Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); + + ToastContentBuilder builder = new ToastContentBuilder(); + + // Act + ToastContentBuilder anotherReference = builder.AddButton(testInputTextBoxId, testButtonContent, testToastActivationType, testButtonLaunchArgs, testImageUriSrc); + + // Assert + Assert.AreSame(builder, anotherReference); + + var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; + Assert.AreEqual(testInputTextBoxId, button.TextBoxId); + Assert.AreEqual(testButtonContent, button.Content); + Assert.AreEqual(testToastActivationType, button.ActivationType); + Assert.AreEqual(testButtonLaunchArgs.ToString(), button.Arguments); + Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); + } + [TestMethod] public void AddInputTextBoxTest_WithStringIdOnly_ReturnSelfWithInputTextBoxAdded() { diff --git a/UnitTests/UnitTests.Notifications.Shared/UnitTests.Notifications.Shared.projitems b/UnitTests/UnitTests.Notifications.Shared/UnitTests.Notifications.Shared.projitems index cb3dc791458..e3eda81fc0e 100644 --- a/UnitTests/UnitTests.Notifications.Shared/UnitTests.Notifications.Shared.projitems +++ b/UnitTests/UnitTests.Notifications.Shared/UnitTests.Notifications.Shared.projitems @@ -11,6 +11,7 @@ + From e8f8d70ff9d808eed79456f168c31a1ef122bebb Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Fri, 30 Oct 2020 10:20:56 -0600 Subject: [PATCH 39/46] Update toast samples to use ToastArguments --- .../SamplePages/Toast/ToastCode.bind | 4 +++- .../SamplePages/Toast/ToastPage.xaml.cs | 4 +++- .../WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind | 4 +++- .../WeatherLiveTileAndToastPage.xaml.cs | 4 +++- 4 files changed, 12 insertions(+), 4 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind index 30c2793d052..80b107e1ec9 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind @@ -2,7 +2,9 @@ private void PopToast() { // Generate the toast notification content and pop the toast new ToastContentBuilder().SetToastScenario(ToastScenario.Reminder) - .AddToastActivationInfo("action=viewEvent&eventId=1983") + .AddToastActivationInfo(new ToastArguments() + .Set("action", "viewEvent") + .Set("eventId", 1983)) .AddText("Adaptive Tiles Meeting") .AddText("Conf Room 2001 / Building 135") .AddText("10:00 AM - 10:30 AM") diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs index 440a81f313e..27ed11d6486 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs @@ -29,7 +29,9 @@ public ToastPage() public static ToastContent GenerateToastContent() { var builder = new ToastContentBuilder().SetToastScenario(ToastScenario.Reminder) - .AddToastActivationInfo("action=viewEvent&eventId=1983", ToastActivationType.Foreground) + .AddToastActivationInfo(new ToastArguments() + .Set("action", "viewEvent") + .Set("eventId", 1983)) .AddText("Adaptive Tiles Meeting") .AddText("Conf Room 2001 / Building 135") .AddText("10:00 AM - 10:30 AM") diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind index 481e3b90646..279f5a25276 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind @@ -4,7 +4,9 @@ private void PopToast() ToastContentBuilder builder = new ToastContentBuilder(); // Include launch string so we know what to open when user clicks toast - builder.AddToastActivationInfo("action=viewForecast&zip=98008"); + builder.AddToastActivationInfo(new ToastArguments() + .Set("action", "viewForecast") + .Set("zip", 98008)); // We'll always have this summary text on our toast notification // (it is required that your toast starts with a text element) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs index b20f5df4584..67b578f4ffa 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs @@ -29,7 +29,9 @@ public static ToastContent GenerateToastContent() ToastContentBuilder builder = new ToastContentBuilder(); // Include launch string so we know what to open when user clicks toast - builder.AddToastActivationInfo("action=viewForecast&zip=98008", ToastActivationType.Foreground); + builder.AddToastActivationInfo(new ToastArguments() + .Set("action", "viewForecast") + .Set("zip", 98008)); // We'll always have this summary text on our toast notification // (it is required that your toast starts with a text element) From 02ff3a88a99ff5dda2c44d289ee9e40efaf621eb Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Fri, 30 Oct 2020 11:21:41 -0600 Subject: [PATCH 40/46] Fixed bug in TileBuilder small tile content The enum flags were incorrectly specified such that when adding content to Medium/Wide/Large tiles, it also added the content to small tiles, which was resulting in the small weather tile not appearing correctly. Note that the small weather tile preview is still missing the temperature (must be bug in NotificationsVisualizerLibrary), but when pinned to Start it looks correct! --- Microsoft.Toolkit.Uwp.Notifications/Tiles/TileCommon.cs | 8 ++++---- .../WeatherLiveTileAndToastCode.bind | 4 ++-- .../WeatherLiveTileAndToastPage.xaml.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.Notifications/Tiles/TileCommon.cs b/Microsoft.Toolkit.Uwp.Notifications/Tiles/TileCommon.cs index c2aabf2f040..c40e030f69d 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Tiles/TileCommon.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Tiles/TileCommon.cs @@ -15,21 +15,21 @@ public enum TileSize /// /// Small Square Tile /// - Small = 0, + Small = 1, /// /// Medium Square Tile /// - Medium = 1, + Medium = 2, /// /// Wide Rectangle Tile /// - Wide = 2, + Wide = 4, /// /// Large Square Tile /// - Large = 4 + Large = 8 } } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind index 279f5a25276..730783c8479 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind @@ -75,8 +75,8 @@ public static TileContent GenerateTileContent() // Small Tile builder.AddTile(Notifications.TileSize.Small) .SetTextStacking(TileTextStacking.Center, Notifications.TileSize.Small) - .AddText("Mon", hintStyle: AdaptiveTextStyle.Body, hintAlign: AdaptiveTextAlign.Center) - .AddText("63°", hintStyle: AdaptiveTextStyle.Base, hintAlign: AdaptiveTextAlign.Center); + .AddText("Mon", Notifications.TileSize.Small, hintStyle: AdaptiveTextStyle.Body, hintAlign: AdaptiveTextAlign.Center) + .AddText("63°", Notifications.TileSize.Small, hintStyle: AdaptiveTextStyle.Base, hintAlign: AdaptiveTextAlign.Center); // Medium Tile builder.AddTile(Notifications.TileSize.Medium) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs index 67b578f4ffa..735b2b6db48 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs @@ -75,8 +75,8 @@ public static TileContent GenerateTileContent() // Small Tile builder.AddTile(Notifications.TileSize.Small) .SetTextStacking(TileTextStacking.Center, Notifications.TileSize.Small) - .AddText("Mon", hintStyle: AdaptiveTextStyle.Body, hintAlign: AdaptiveTextAlign.Center) - .AddText("63°", hintStyle: AdaptiveTextStyle.Base, hintAlign: AdaptiveTextAlign.Center); + .AddText("Mon", Notifications.TileSize.Small, hintStyle: AdaptiveTextStyle.Body, hintAlign: AdaptiveTextAlign.Center) + .AddText("63°", Notifications.TileSize.Small, hintStyle: AdaptiveTextStyle.Base, hintAlign: AdaptiveTextAlign.Center); // Medium Tile builder.AddTile(Notifications.TileSize.Medium) From 8956b81ebb90cebf1d3554e62fca68fa92015a2c Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Fri, 30 Oct 2020 11:48:24 -0600 Subject: [PATCH 41/46] Add headers --- .../Toasts/ToastArguments.cs | 6 +++++- Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs | 6 +++--- .../UnitTests.Notifications.Shared/TestToastArguments.cs | 6 +++++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs index fb61aea283a..989146ba55f 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections; using System.Collections.Generic; using System.Linq; diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs index dd93d2d1865..13711b8ffdf 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs @@ -1,6 +1,6 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. namespace Microsoft.Toolkit.Win32.WpfCore.SampleApp { diff --git a/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs b/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs index 7f170cf3ada..4b7eeb632e7 100644 --- a/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs +++ b/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs @@ -1,4 +1,8 @@ -using System; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; using System.Collections.Generic; using System.Linq; using Microsoft.Toolkit.Uwp.Notifications; From f1a3f022022aaedacd550985a697dcfd2e0d3de2 Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Wed, 4 Nov 2020 15:12:59 -0800 Subject: [PATCH 42/46] Update qmatteoq license --- ThirdPartyNotices.txt | 30 ++---------------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/ThirdPartyNotices.txt b/ThirdPartyNotices.txt index 0fca67436eb..ec7322617dd 100644 --- a/ThirdPartyNotices.txt +++ b/ThirdPartyNotices.txt @@ -10,7 +10,7 @@ This project incorporates components from the projects listed below. The origina 3. lbugnion/mvvmlight commit 4cbf77c (https://github.com/lbugnion/mvvmlight), from which some APIs from the `Microsoft.Toolkit.Mvvm` package take inspiration from. 4. PrivateObject/PrivateType (https://github.com/microsoft/testfx/tree/664ac7c2ac9dbfbee9d2a0ef560cfd72449dfe34/src/TestFramework/Extension.Desktop), included in UnitTests. 5. QuinnDamerell/UniversalMarkdown (https://github.com/QuinnDamerell/UniversalMarkdown) contributed by Quinn Damerell and Paul Bartrum for the MarkdownTextBlock control, relicensed to this .NET Foundation project under the MIT license upon contribution in https://github.com/windows-toolkit/WindowsCommunityToolkit/pull/772. -6. qmatteoq/DesktopBridgeHelpers commit e278153 (https://github.com/qmatteoq/DesktopBridgeHelpers), using in DesktopNotificationManagerCompat.cs and DesktopBridgeHelpers.cs to identify if running with identity. +6. qmatteoq/DesktopBridgeHelpers commit e278153 (https://github.com/qmatteoq/DesktopBridgeHelpers), contributed by Matteo Pagani to identify if running with identity in DesktopNotificationManagerCompat.cs and DesktopBridgeHelpers.cs, relicensed to this .NET Foundation project under the MIT license upon contribution in https://github.com/windows-toolkit/WindowsCommunityToolkit/pull/3457. %% PedroLamas/DeferredEvents NOTICES AND INFORMATION BEGIN HERE ========================================= @@ -113,30 +113,4 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ========================================= -END OF PrivateOject/PrivateType NOTICES AND INFORMATION - -%% qmatteoq/DesktopBridgeHelpers NOTICES AND INFORMATION BEGIN HERE -========================================= -MIT License - -Copyright (c) Microsoft Corporation. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE -========================================= -END OF qmatteoq/DesktopBridgeHelpers NOTICES AND INFORMATION \ No newline at end of file +END OF PrivateOject/PrivateType NOTICES AND INFORMATION \ No newline at end of file From f9045dee9af7c041fa0be362abedf2bc7e715570 Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Thu, 5 Nov 2020 15:12:32 -0800 Subject: [PATCH 43/46] Simplified generic toast arguments --- .../Builder/ToastContentBuilder.Actions.cs | 57 +++- .../Toasts/Builder/ToastContentBuilder.cs | 194 ++++++++++++-- .../Toasts/ToastArguments.cs | 92 +++---- .../Toasts/ToastButton.cs | 2 +- .../SamplePages/Toast/ToastCode.bind | 4 +- .../SamplePages/Toast/ToastPage.xaml.cs | 7 +- .../WeatherLiveTileAndToastCode.bind | 4 +- .../WeatherLiveTileAndToastPage.xaml.cs | 4 +- .../App.xaml.cs | 32 +-- .../MainWindow.xaml.cs | 20 +- .../MyToastActions.cs | 5 - .../TestToastArguments.cs | 26 +- .../TestToastContentBuilder.cs | 248 ++++++++++++++++-- 13 files changed, 546 insertions(+), 149 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs index 8f35a959b17..49a8818c549 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs @@ -60,7 +60,30 @@ private IList InputList #endif public ToastContentBuilder AddButton(string content, ToastActivationType activationType, ToastArguments arguments) { - return AddButton(content, activationType, arguments.ToString()); + AddButton(content, activationType, SerializeArgumentsIncludingGeneric(arguments)); + + // Remove this button from the custom arguments list + _buttonsUsingCustomArguments.RemoveAt(_buttonsUsingCustomArguments.Count - 1); + + return this; + } + + private string SerializeArgumentsIncludingGeneric(ToastArguments arguments) + { + if (_genericArguments.Count == 0) + { + return arguments.ToString(); + } + + foreach (var genericArg in _genericArguments) + { + if (!arguments.Contains(genericArg.Key)) + { + arguments.Add(genericArg.Key, genericArg.Value); + } + } + + return arguments.ToString(); } /// @@ -76,7 +99,12 @@ public ToastContentBuilder AddButton(string content, ToastActivationType activat #endif public ToastContentBuilder AddButton(string content, ToastActivationType activationType, ToastArguments arguments, Uri imageUri) { - return AddButton(content, activationType, arguments.ToString(), imageUri); + AddButton(content, activationType, SerializeArgumentsIncludingGeneric(arguments), imageUri); + + // Remove this button from the custom arguments list + _buttonsUsingCustomArguments.RemoveAt(_buttonsUsingCustomArguments.Count - 1); + + return this; } /// @@ -91,6 +119,19 @@ public ToastContentBuilder AddButton(string content, ToastActivationType activat return AddButton(content, activationType, arguments, default); } + /// + /// Add an button to the toast that will be display to the right of the input text box, achieving a quick reply scenario. + /// + /// ID of an existing in order to have this button display to the right of the input, achieving a quick reply scenario. + /// Text to display on the button. + /// Type of activation this button will use when clicked. Defaults to Foreground. + /// App-defined arguments that the app can later retrieve once it is activated when the user clicks the button. + /// The current instance of + public ToastContentBuilder AddButton(string textBoxId, string content, ToastActivationType activationType, ToastArguments arguments) + { + return AddButton(textBoxId, content, activationType, arguments, default); + } + /// /// Add a button to the current toast. /// @@ -129,7 +170,12 @@ public ToastContentBuilder AddButton(string content, ToastActivationType activat #endif public ToastContentBuilder AddButton(string textBoxId, string content, ToastActivationType activationType, ToastArguments arguments, Uri imageUri) { - return AddButton(textBoxId, content, activationType, arguments.ToString(), imageUri); + AddButton(textBoxId, content, activationType, SerializeArgumentsIncludingGeneric(arguments), imageUri); + + // Remove this button from the custom arguments list + _buttonsUsingCustomArguments.RemoveAt(_buttonsUsingCustomArguments.Count - 1); + + return this; } /// @@ -147,6 +193,11 @@ public ToastContentBuilder AddButton(IToastButton button) ButtonList.Add(button); + if (button is ToastButton b) + { + _buttonsUsingCustomArguments.Add(b); + } + return this; } diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs index 3f635cdae6d..be4ed3205c4 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; namespace Microsoft.Toolkit.Uwp.Notifications { @@ -11,6 +12,12 @@ namespace Microsoft.Toolkit.Uwp.Notifications /// public partial class ToastContentBuilder { + private Dictionary _genericArguments = new Dictionary(); + + private bool _customArgumentsUsedOnToastItself; + + private List _buttonsUsingCustomArguments = new List(); + /// /// Gets internal instance of . This is equivalent to the call to . /// @@ -44,6 +51,22 @@ public ToastContentBuilder AddCustomTimeStamp( return this; } + /// + /// Add a header to a toast. + /// + /// A developer-created identifier that uniquely identifies this header. If two notifications have the same header id, they will be displayed underneath the same header in Action Center. + /// A title for the header. + /// Developer-defined arguments that are returned to the app when the user clicks this header. + /// The current instance of + /// More info about toast header: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-headers +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif + public ToastContentBuilder AddHeader(string id, string title, ToastArguments arguments) + { + return AddHeader(id, title, arguments.ToString()); + } + /// /// Add a header to a toast. /// @@ -60,44 +83,184 @@ public ToastContentBuilder AddHeader(string id, string title, string arguments) } /// - /// Add info that can be used by the application when the app was activated/launched by the toast. Uses foreground activation. + /// Adds a key (without value) to the activation arguments that will be returned when the toast notification or its buttons are clicked. /// - /// Custom app-defined launch arguments to be passed along on toast activation + /// The key. + /// The current instance of + public ToastContentBuilder AddArgument(string key) + { + return AddArgumentHelper(key, null); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. /// The current instance of #if WINRT [Windows.Foundation.Metadata.DefaultOverload] + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] #endif - public ToastContentBuilder AddToastActivationInfo(ToastArguments launchArgs) + public ToastContentBuilder AddArgument(string key, string value) { - return AddToastActivationInfo(launchArgs.ToString()); + return AddArgumentHelper(key, value); } /// - /// Add info that can be used by the application when the app was activated/launched by the toast. + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. /// - /// Custom app-defined launch arguments to be passed along on toast activation - /// Set the activation type that will be used when the user click on this toast + /// The key for this value. + /// The value itself. /// The current instance of #if WINRT - [Windows.Foundation.Metadata.DefaultOverload] + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] #endif - public ToastContentBuilder AddToastActivationInfo(ToastArguments launchArgs, ToastActivationType activationType) + public ToastContentBuilder AddArgument(string key, int value) { - return AddToastActivationInfo(launchArgs.ToString(), activationType); + return AddArgumentHelper(key, value.ToString()); } /// - /// Add info that can be used by the application when the app was activated/launched by the toast. Uses foreground activation. + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, double value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, float value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastContentBuilder")] +#endif + public ToastContentBuilder AddArgument(string key, bool value) + { + return AddArgumentHelper(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space + } + +#if !WINRT + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current instance of + public ToastContentBuilder AddArgument(string key, Enum value) + { + return AddArgumentHelper(key, ((int)(object)value).ToString()); + } +#endif + + private ToastContentBuilder AddArgumentHelper(string key, string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + bool alreadyExists = _genericArguments.ContainsKey(key); + + _genericArguments[key] = value; + + if (Content.ActivationType != ToastActivationType.Protocol && !_customArgumentsUsedOnToastItself) + { + Content.Launch = alreadyExists ? SerializeArgumentsHelper(_genericArguments) : AddArgumentHelper(Content.Launch, key, value); + } + + if (Content.Actions is ToastActionsCustom actions) + { + foreach (var button in actions.Buttons) + { + if (button is ToastButton b && b.ActivationType != ToastActivationType.Protocol && !_buttonsUsingCustomArguments.Contains(b)) + { + var bArgs = ToastArguments.Parse(b.Arguments); + if (!bArgs.Contains(key)) + { + bArgs.Add(key, value); + b.Arguments = bArgs.ToString(); + } + } + } + } + + return this; + } + + private string SerializeArgumentsHelper(IDictionary arguments) + { + var args = new ToastArguments(); + + foreach (var a in arguments) + { + args.Add(a.Key, a.Value); + } + + return args.ToString(); + } + + private string AddArgumentHelper(string existing, string key, string value) + { + string pair = ToastArguments.EncodePair(key, value); + + if (existing == null) + { + return pair; + } + else + { + return existing + ToastArguments.Separator + pair; + } + } + + /// + /// Configures the toast notification to launch the specified url when the toast body is clicked. + /// + /// The protocol to launch. + /// The current instance of + public ToastContentBuilder SetProtocolActivation(Uri protocol) + { + Content.Launch = protocol.ToString(); + Content.ActivationType = ToastActivationType.Protocol; + return this; + } + + /// + /// Configures the toast notification to use background activation when the toast body is clicked. /// - /// Custom app-defined launch arguments to be passed along on toast activation /// The current instance of - public ToastContentBuilder AddToastActivationInfo(string launchArgs) + public ToastContentBuilder SetBackgroundActivation() { - return AddToastActivationInfo(launchArgs, ToastActivationType.Foreground); + Content.ActivationType = ToastActivationType.Background; + return this; } /// - /// Add info that can be used by the application when the app was activated/launched by the toast. + /// Instead of this method, for foreground/background activation, it is suggested to use and optionally . For protocol activation, you should use . Add info that can be used by the application when the app was activated/launched by the toast. /// /// Custom app-defined launch arguments to be passed along on toast activation /// Set the activation type that will be used when the user click on this toast @@ -106,6 +269,7 @@ public ToastContentBuilder AddToastActivationInfo(string launchArgs, ToastActiva { Content.Launch = launchArgs; Content.ActivationType = activationType; + _customArgumentsUsedOnToastItself = true; return this; } diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs index 989146ba55f..f5dd09b95f4 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastArguments.cs @@ -16,6 +16,14 @@ public sealed class ToastArguments : IEnumerable> { private Dictionary _dictionary = new Dictionary(); + internal ToastArguments Clone() + { + return new ToastArguments() + { + _dictionary = new Dictionary(_dictionary) + }; + } + #if !WINRT /// /// Gets the value of the specified key. Throws if the key could not be found. @@ -186,43 +194,14 @@ public T GetEnum(string key) public int Count => _dictionary.Count; /// - /// Adds a key (without a value) to the arguments. If the key already exists, throws an exception. - /// - /// The name of the parameter. - public void Add(string key) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - _dictionary.Add(key, null); - } - - /// - /// Adds a key and optional value to the arguments. If the key already exists, throws an exception. - /// - /// The name of the parameter. - /// The optional value of the key. - public void Add(string key, string value) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - _dictionary.Add(key, value); - } - - /// - /// Sets a key. If there is an existing key, it is replaced. + /// Adds a key. If there is an existing key, it is replaced. /// /// The key. /// The current object. #if WINRT [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] #endif - public ToastArguments Set(string key) + public ToastArguments Add(string key) { if (key == null) { @@ -235,7 +214,7 @@ public ToastArguments Set(string key) } /// - /// Sets a key and optional value. If there is an existing key, it is replaced. + /// Adds a key and optional value. If there is an existing key, it is replaced. /// /// The key. /// The optional value of the key. @@ -244,7 +223,7 @@ public ToastArguments Set(string key) [Windows.Foundation.Metadata.DefaultOverload] [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] #endif - public ToastArguments Set(string key, string value) + public ToastArguments Add(string key, string value) { if (key == null) { @@ -257,7 +236,7 @@ public ToastArguments Set(string key, string value) } /// - /// Sets a key and value. If there is an existing key, it is replaced. + /// Adds a key and value. If there is an existing key, it is replaced. /// /// The key. /// The value of the key. @@ -265,13 +244,13 @@ public ToastArguments Set(string key, string value) #if WINRT [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] #endif - public ToastArguments Set(string key, int value) + public ToastArguments Add(string key, int value) { - return SetHelper(key, value); + return AddHelper(key, value); } /// - /// Sets a key and value. If there is an existing key, it is replaced. + /// Adds a key and value. If there is an existing key, it is replaced. /// /// The key. /// The value of the key. @@ -279,13 +258,13 @@ public ToastArguments Set(string key, int value) #if WINRT [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] #endif - public ToastArguments Set(string key, double value) + public ToastArguments Add(string key, double value) { - return SetHelper(key, value); + return AddHelper(key, value); } /// - /// Sets a key and value. If there is an existing key, it is replaced. + /// Adds a key and value. If there is an existing key, it is replaced. /// /// The key. /// The value of the key. @@ -293,13 +272,13 @@ public ToastArguments Set(string key, double value) #if WINRT [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] #endif - public ToastArguments Set(string key, float value) + public ToastArguments Add(string key, float value) { - return SetHelper(key, value); + return AddHelper(key, value); } /// - /// Sets a key and value. If there is an existing key, it is replaced. + /// Adds a key and value. If there is an existing key, it is replaced. /// /// The key. /// The value of the key. @@ -307,25 +286,25 @@ public ToastArguments Set(string key, float value) #if WINRT [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("toastArguments")] #endif - public ToastArguments Set(string key, bool value) + public ToastArguments Add(string key, bool value) { - return Set(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space + return Add(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space } #if !WINRT /// - /// Sets a key and value. If there is an existing key, it is replaced. + /// Adds a key and value. If there is an existing key, it is replaced. /// /// The key. /// The value of the key. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. /// The current object. - public ToastArguments Set(string key, Enum value) + public ToastArguments Add(string key, Enum value) { - return Set(key, (int)(object)value); + return Add(key, (int)(object)value); } #endif - private ToastArguments SetHelper(string key, object value) + private ToastArguments AddHelper(string key, object value) { if (key == null) { @@ -448,15 +427,20 @@ public static ToastArguments Parse(string toastArgumentsStr) /// A string that can be used within a toast notification. public sealed override string ToString() { - return string.Join(";", this.Select(pair => + return string.Join(Separator, this.Select(pair => EncodePair(pair.Key, pair.Value))); + } - // Key - Encode(pair.Key) + + internal static string EncodePair(string key, string value) + { + // Key + return Encode(key) + - // Write value if not null - ((pair.Value == null) ? string.Empty : ("=" + Encode(pair.Value))))); + // Write value if not null + ((value == null) ? string.Empty : ("=" + Encode(value))); } + internal const string Separator = ";"; + /// /// Gets an enumerator to enumerate the arguments. Note that order of the arguments is NOT preserved. /// diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs index 522a5c254c9..bc9fd89b265 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs @@ -41,7 +41,7 @@ public ToastButton(string content, string arguments) /// Gets app-defined string of arguments that the app can later retrieve once it is /// activated when the user clicks the button. Required /// - public string Arguments { get; private set; } + public string Arguments { get; internal set; } /// /// Gets or sets what type of activation this button will use when clicked. Defaults to Foreground. diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind index 80b107e1ec9..59da8ba547d 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind @@ -2,9 +2,7 @@ private void PopToast() { // Generate the toast notification content and pop the toast new ToastContentBuilder().SetToastScenario(ToastScenario.Reminder) - .AddToastActivationInfo(new ToastArguments() - .Set("action", "viewEvent") - .Set("eventId", 1983)) + .AddArgument("eventId", 1983) .AddText("Adaptive Tiles Meeting") .AddText("Conf Room 2001 / Building 135") .AddText("10:00 AM - 10:30 AM") diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs index 27ed11d6486..1843f424a0d 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs @@ -28,10 +28,9 @@ public ToastPage() public static ToastContent GenerateToastContent() { - var builder = new ToastContentBuilder().SetToastScenario(ToastScenario.Reminder) - .AddToastActivationInfo(new ToastArguments() - .Set("action", "viewEvent") - .Set("eventId", 1983)) + var builder = new ToastContentBuilder() + .SetToastScenario(ToastScenario.Reminder) + .AddArgument("eventId", 1983) .AddText("Adaptive Tiles Meeting") .AddText("Conf Room 2001 / Building 135") .AddText("10:00 AM - 10:30 AM") diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind index 730783c8479..6e05a17e505 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind @@ -4,9 +4,7 @@ private void PopToast() ToastContentBuilder builder = new ToastContentBuilder(); // Include launch string so we know what to open when user clicks toast - builder.AddToastActivationInfo(new ToastArguments() - .Set("action", "viewForecast") - .Set("zip", 98008)); + builder.AddArgument("zip", 98008); // We'll always have this summary text on our toast notification // (it is required that your toast starts with a text element) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs index 735b2b6db48..22ed38e811b 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs @@ -29,9 +29,7 @@ public static ToastContent GenerateToastContent() ToastContentBuilder builder = new ToastContentBuilder(); // Include launch string so we know what to open when user clicks toast - builder.AddToastActivationInfo(new ToastArguments() - .Set("action", "viewForecast") - .Set("zip", 98008)); + builder.AddArgument("zip", 98008); // We'll always have this summary text on our toast notification // (it is required that your toast starts with a text element) diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs index 6e587b53bf2..d78b39202d8 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs @@ -47,8 +47,18 @@ private void ToastNotificationManagerCompat_OnActivated(ToastNotificationActivat // Parse the toast arguments ToastArguments args = ToastArguments.Parse(e.Argument); - // See what action is being requested - if (args.TryGetValue("action", out MyToastActions action)) + int conversationId = args.GetInt("conversationId"); + + // If no specific action, view the conversation + if (!args.TryGetValue("action", out MyToastActions action)) + { + // Make sure we have a window open and in foreground + OpenWindowIfNeeded(); + + // And then show the conversation + (Current.Windows[0] as MainWindow).ShowConversation(conversationId); + } + else { switch (action) { @@ -66,20 +76,6 @@ private void ToastNotificationManagerCompat_OnActivated(ToastNotificationActivat break; - // Open the conversation - case MyToastActions.ViewConversation: - - // The conversation ID retrieved from the toast args - int conversationId = args.GetInt("conversationId"); - - // Make sure we have a window open and in foreground - OpenWindowIfNeeded(); - - // And then show the conversation - (Current.Windows[0] as MainWindow).ShowConversation(); - - break; - // Background: Quick reply to the conversation case MyToastActions.Reply: @@ -87,7 +83,7 @@ private void ToastNotificationManagerCompat_OnActivated(ToastNotificationActivat string msg = e.UserInput["tbReply"] as string; // And send this message - ShowToast("Message sent: " + msg); + ShowToast("Message sent: " + msg + "\nconversationId: " + conversationId); // If there's no windows open, exit the app if (Current.Windows.Count == 0) @@ -100,7 +96,7 @@ private void ToastNotificationManagerCompat_OnActivated(ToastNotificationActivat // Background: Send a like case MyToastActions.Like: - ShowToast("Like sent!"); + ShowToast($"Like sent to conversation {conversationId}!"); // If there's no windows open, exit the app if (Current.Windows.Count == 0) diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs index 84cb107ffdb..49d9c9227cc 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs @@ -35,10 +35,8 @@ private async void ButtonPopToast_Click(object sender, RoutedEventArgs e) // Construct the toast content and show it! new ToastContentBuilder() - // Arguments when the user taps body of toast - .AddToastActivationInfo(new ToastArguments() - .Set("action", MyToastActions.ViewConversation) - .Set("conversationId", conversationId)) + // Arguments that are returned when the user clicks the toast or a button + .AddArgument("conversationId", conversationId) // Visual content .AddText(title) @@ -51,16 +49,14 @@ private async void ButtonPopToast_Click(object sender, RoutedEventArgs e) // Buttons .AddButton("Reply", ToastActivationType.Background, new ToastArguments() - .Set("action", MyToastActions.Reply) - .Set("conversationId", conversationId)) + .Add("action", MyToastActions.Reply)) .AddButton("Like", ToastActivationType.Background, new ToastArguments() - .Set("action", MyToastActions.Like) - .Set("conversationId", conversationId)) + .Add("action", MyToastActions.Like)) .AddButton("View", ToastActivationType.Foreground, new ToastArguments() - .Set("action", MyToastActions.ViewImage) - .Set("imageUrl", image)) + .Add("action", MyToastActions.ViewImage) + .Add("imageUrl", image)) // And show the toast! .Show(); @@ -121,11 +117,11 @@ private static async Task DownloadImageToDisk(string httpImage) } } - internal void ShowConversation() + internal void ShowConversation(int conversationId) { ContentBody.Content = new TextBlock() { - Text = "You've just opened a conversation!", + Text = "You've just opened conversation " + conversationId, FontWeight = FontWeights.Bold }; } diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs index 13711b8ffdf..acdaf7dc5a7 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs @@ -6,11 +6,6 @@ namespace Microsoft.Toolkit.Win32.WpfCore.SampleApp { public enum MyToastActions { - /// - /// View the conversation - /// - ViewConversation, - /// /// Inline reply to a message /// diff --git a/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs b/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs index 4b7eeb632e7..d7d9abec910 100644 --- a/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs +++ b/UnitTests/UnitTests.Notifications.Shared/TestToastArguments.cs @@ -261,7 +261,7 @@ public void TestContains() Assert.IsTrue(qs.Contains("isBook", null)); Assert.IsFalse(qs.Contains("isBook", "True")); - qs.Set("isBook", "True"); + qs.Add("isBook", "True"); Assert.IsTrue(qs.Contains("isBook")); Assert.IsFalse(qs.Contains("isBook", null)); @@ -278,11 +278,11 @@ public void TestContains() } [TestMethod] - public void TestSet() + public void TestAdd() { ToastArguments qs = new ToastArguments(); - qs.Set("name", "Andrew"); + qs.Add("name", "Andrew"); AssertEqual( new ToastArguments() @@ -290,7 +290,7 @@ public void TestSet() { "name", "Andrew" } }, qs); - qs.Set("age", "22"); + qs.Add("age", "22"); AssertEqual( new ToastArguments() @@ -299,7 +299,7 @@ public void TestSet() { "age", "22" } }, qs); - qs.Set("name", "Lei"); + qs.Add("name", "Lei"); AssertEqual( new ToastArguments() @@ -309,7 +309,7 @@ public void TestSet() }, qs); string nullStr = null; - qs.Set("name", nullStr); + qs.Add("name", nullStr); AssertEqual( new ToastArguments() @@ -361,15 +361,15 @@ public void TestCount() public void TestStronglyTyped() { ToastArguments args = new ToastArguments() - .Set("isAdult", true) - .Set("isPremium", false) - .Set("age", 22) - .Set("level", 0) - .Set("gpa", 3.97) - .Set("percent", 97.3f); + .Add("isAdult", true) + .Add("isPremium", false) + .Add("age", 22) + .Add("level", 0) + .Add("gpa", 3.97) + .Add("percent", 97.3f); #if !WINRT - args.Set("activationKind", ToastActivationType.Background); + args.Add("activationKind", ToastActivationType.Background); #endif AssertEqual( diff --git a/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs b/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs index 89d55e4060f..7a57a850214 100644 --- a/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs +++ b/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs @@ -47,6 +47,27 @@ public void AddHeaderTest_WithExpectedArgs_ReturnSelfWithHeaderAdded() Assert.AreEqual(testToastArguments, builder.Content.Header.Arguments); } + [TestMethod] + public void AddHeaderTest_WithExpectedArgsAndToastArguments_ReturnSelfWithHeaderAdded() + { + // Arrange + string testToastHeaderId = "Test Header ID"; + string testToastTitle = "Test Toast Title"; + ToastArguments testToastArguments = new ToastArguments() + .Add("arg1", 5) + .Add("arg2", "tacos"); + + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder.AddHeader(testToastHeaderId, testToastTitle, testToastArguments); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual(testToastHeaderId, builder.Content.Header.Id); + Assert.AreEqual(testToastTitle, builder.Content.Header.Title); + Assert.AreEqual(testToastArguments.ToString(), builder.Content.Header.Arguments); + } + [TestMethod] public void AddToastActivationInfoTest_WithExpectedArgs_ReturnSelfWithActivationInfoAdded() { @@ -65,36 +86,233 @@ public void AddToastActivationInfoTest_WithExpectedArgs_ReturnSelfWithActivation } [TestMethod] - public void AddToastActivationInfoDefaultTest_WithExpectedArgs_ReturnSelfWithActivationInfoAdded() + public void AddArgumentTest_Basic_ReturnSelfWithArgumentsAdded() { - // Arrange - string testToastLaunchArugments = "Test Toast Launch Args"; + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("userId", 542) + .AddArgument("name", "Andrew"); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("userId=542;name=Andrew", builder.Content.Launch); + } + [TestMethod] + public void AddArgumentTest_NoValue_ReturnSelfWithArgumentsAdded() + { // Act ToastContentBuilder builder = new ToastContentBuilder(); - ToastContentBuilder anotherReference = builder.AddToastActivationInfo(testToastLaunchArugments); + ToastContentBuilder anotherReference = builder + .AddArgument("isPurelyInformational"); // Assert Assert.AreSame(builder, anotherReference); - Assert.AreEqual(testToastLaunchArugments, builder.Content.Launch); - Assert.AreEqual(ToastActivationType.Foreground, builder.Content.ActivationType); + Assert.AreEqual("isPurelyInformational", builder.Content.Launch); } [TestMethod] - public void AddToastActivationInfoTest_WithExpectedArgs_UsingToastArguments() + public void AddArgumentTest_Escaping_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("user;Id", "andrew=leader%26bares"); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("user%3BId=andrew%3Dleader%2526bares", builder.Content.Launch); + } + + [TestMethod] + public void AddArgumentTest_Replacing_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("userId", 542) + .AddArgument("name", "Andrew") + .AddArgument("userId", 601); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("userId=601;name=Andrew", builder.Content.Launch); + } + + [TestMethod] + public void AddArgumentTest_Generic_ReturnSelfWithArgumentsAdded() { // Arrange - ToastArguments args = new ToastArguments().Set("name", "Andrew"); - ToastActivationType testToastActivationType = ToastActivationType.Background; + const string userIdKey = "userId"; + const int userIdValue = 542; // Act ToastContentBuilder builder = new ToastContentBuilder(); - ToastContentBuilder anotherReference = builder.AddToastActivationInfo(args, testToastActivationType); + ToastContentBuilder anotherReference = builder + .AddButton("Accept", ToastActivationType.Background, new ToastArguments() + .Add("action", "accept")) + .AddButton(new ToastButtonSnooze()) + .AddButton("View", ToastActivationType.Protocol, "https://msn.com") + + // Add generic arguments halfway through (should be applied to existing buttons and to any subsequent buttons added later) + .AddArgument(userIdKey, userIdValue) + + .AddButton("Decline", ToastActivationType.Background, new ToastArguments() + .Add("action", "decline")) + .AddButton("Report", ToastActivationType.Protocol, "https://microsoft.com"); // Assert Assert.AreSame(builder, anotherReference); - Assert.AreEqual(args.ToString(), builder.Content.Launch); - Assert.AreEqual(testToastActivationType, builder.Content.ActivationType); + + // Top level arguments should be present + Assert.AreEqual("userId=542", builder.Content.Launch); + + // All foreground/background activation buttons should have received generic arguments. Protocol and system activation buttons shouldn't have had any arguments changed. + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("action=accept;userId=542", button1.Arguments); + + var button2 = actions.Buttons[1]; + Assert.IsInstanceOfType(button2, typeof(ToastButtonSnooze)); + + var button3 = actions.Buttons[2] as ToastButton; + Assert.AreEqual("View", button3.Content); + Assert.AreEqual("https://msn.com", button3.Arguments); + + var button4 = actions.Buttons[3] as ToastButton; + Assert.AreEqual("Decline", button4.Content); + Assert.AreEqual("action=decline;userId=542", button4.Arguments); + + var button5 = actions.Buttons[4] as ToastButton; + Assert.AreEqual("Report", button5.Content); + Assert.AreEqual("https://microsoft.com", button5.Arguments); + } + + [TestMethod] + public void AddArgumentTest_ReplacingWithinButton_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddButton("Accept", ToastActivationType.Background, new ToastArguments() + .Add("action", "accept") + .Add("userId", 601)) + + // Add generic arguments halfway through (should be applied to existing buttons and to any subsequent buttons added later) + .AddArgument("userId", 542) + + .AddButton("Decline", ToastActivationType.Background, new ToastArguments() + .Add("action", "decline") + .Add("userId", 601)); + + // Assert + Assert.AreSame(builder, anotherReference); + + // Top level arguments should be present + Assert.AreEqual("userId=542", builder.Content.Launch); + + // Buttons should have overridden the generic userId + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("action=accept;userId=601", button1.Arguments); + + var button2 = actions.Buttons[1] as ToastButton; + Assert.AreEqual("Decline", button2.Content); + Assert.AreEqual("action=decline;userId=601", button2.Arguments); + } + + [TestMethod] + public void AddArgumentTest_AvoidModifyingCustomButtons_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddToastActivationInfo("myCustomLaunchStr", ToastActivationType.Foreground) + + .AddButton("Accept", ToastActivationType.Background, "myAcceptStr") + + // userId shouldn't be added to any of these except view + .AddArgument("userId", 542) + + .AddButton("Decline", ToastActivationType.Background, "myDeclineStr") + + .AddButton("View", ToastActivationType.Foreground, new ToastArguments() + .Add("action", "view")); + + // Assert + Assert.AreSame(builder, anotherReference); + + // Top level arguments should be the custom string since user set that + Assert.AreEqual("myCustomLaunchStr", builder.Content.Launch); + + // Buttons should have their custom strings except the last + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("myAcceptStr", button1.Arguments); + + var button2 = actions.Buttons[1] as ToastButton; + Assert.AreEqual("Decline", button2.Content); + Assert.AreEqual("myDeclineStr", button2.Arguments); + + var button3 = actions.Buttons[2] as ToastButton; + Assert.AreEqual("View", button3.Content); + Assert.AreEqual("action=view;userId=542", button3.Arguments); + } + + [TestMethod] + public void AddArgumentTest_BackgroundActivation_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddArgument("userId", 542) + .SetBackgroundActivation(); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("userId=542", builder.Content.Launch); + Assert.AreEqual(ToastActivationType.Background, builder.Content.ActivationType); + } + + [TestMethod] + public void SetProtocolActivationTest_ReturnSelfWithArgumentsAdded() + { + // Act + ToastContentBuilder builder = new ToastContentBuilder(); + ToastContentBuilder anotherReference = builder + .AddButton("Accept", ToastActivationType.Background, new ToastArguments() + .Add("action", "accept")) + + .AddArgument("userId", 542) + + .SetProtocolActivation(new Uri("https://msn.com/")) + + .AddArgument("name", "Andrew") + + .AddButton("Decline", ToastActivationType.Background, new ToastArguments() + .Add("action", "decline")); + + // Assert + Assert.AreSame(builder, anotherReference); + Assert.AreEqual("https://msn.com/", builder.Content.Launch); + Assert.AreEqual(ToastActivationType.Protocol, builder.Content.ActivationType); + + var actions = builder.Content.Actions as ToastActionsCustom; + + var button1 = actions.Buttons[0] as ToastButton; + Assert.AreEqual("Accept", button1.Content); + Assert.AreEqual("action=accept;userId=542;name=Andrew", button1.Arguments); + + var button2 = actions.Buttons[1] as ToastButton; + Assert.AreEqual("Decline", button2.Content); + Assert.AreEqual("action=decline;userId=542;name=Andrew", button2.Arguments); } [TestMethod] @@ -568,7 +786,7 @@ public void AddButtonTest_WithTextOnlyButtonAndToastArguments_ReturnSelfWithButt // Arrange string testButtonContent = "Test Button Content"; ToastActivationType testToastActivationType = ToastActivationType.Background; - var testButtonLaunchArgs = new ToastArguments().Set("action", "view"); + var testButtonLaunchArgs = new ToastArguments().Add("action", "view"); ToastContentBuilder builder = new ToastContentBuilder(); // Act @@ -613,7 +831,7 @@ public void AddButtonTest_WithCustomImageAndTextButtonAndToastArguments_ReturnSe // Arrange string testButtonContent = "Test Button Content"; ToastActivationType testToastActivationType = ToastActivationType.Background; - var testButtonLaunchArgs = new ToastArguments().Set("action", "accept"); + var testButtonLaunchArgs = new ToastArguments().Add("action", "accept"); Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); ToastContentBuilder builder = new ToastContentBuilder(); @@ -664,7 +882,7 @@ public void AddButtonTest_WithTextBoxIdAndToastArguments_ReturnSelfWithButtonAdd string testInputTextBoxId = Guid.NewGuid().ToString(); string testButtonContent = "Test Button Content"; ToastActivationType testToastActivationType = ToastActivationType.Background; - var testButtonLaunchArgs = new ToastArguments().Set("action", "send"); + var testButtonLaunchArgs = new ToastArguments().Add("action", "send"); Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); ToastContentBuilder builder = new ToastContentBuilder(); From 89ed383f5db3f8103658d1f1fbb093c14b1d4abc Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Thu, 5 Nov 2020 18:50:28 -0800 Subject: [PATCH 44/46] Add back top level action type in samples --- .../SamplePages/Toast/ToastCode.bind | 1 + .../SamplePages/Toast/ToastPage.xaml.cs | 1 + .../WeatherLiveTileAndToastCode.bind | 1 + .../WeatherLiveTileAndToastPage.xaml.cs | 1 + .../App.xaml.cs | 21 +++++++++++-------- .../MainWindow.xaml.cs | 1 + .../MyToastActions.cs | 5 +++++ 7 files changed, 22 insertions(+), 9 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind index 59da8ba547d..9d72fed3c34 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind @@ -2,6 +2,7 @@ private void PopToast() { // Generate the toast notification content and pop the toast new ToastContentBuilder().SetToastScenario(ToastScenario.Reminder) + .AddArgument("action", "viewEvent") .AddArgument("eventId", 1983) .AddText("Adaptive Tiles Meeting") .AddText("Conf Room 2001 / Building 135") diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs index 1843f424a0d..896795bec19 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastPage.xaml.cs @@ -30,6 +30,7 @@ public static ToastContent GenerateToastContent() { var builder = new ToastContentBuilder() .SetToastScenario(ToastScenario.Reminder) + .AddArgument("action", "viewEvent") .AddArgument("eventId", 1983) .AddText("Adaptive Tiles Meeting") .AddText("Conf Room 2001 / Building 135") diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind index 6e05a17e505..31d86950b79 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastCode.bind @@ -4,6 +4,7 @@ private void PopToast() ToastContentBuilder builder = new ToastContentBuilder(); // Include launch string so we know what to open when user clicks toast + builder.AddArgument("action", "viewForecast"); builder.AddArgument("zip", 98008); // We'll always have this summary text on our toast notification diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs index 22ed38e811b..3efea5c98d4 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/WeatherLiveTileAndToast/WeatherLiveTileAndToastPage.xaml.cs @@ -29,6 +29,7 @@ public static ToastContent GenerateToastContent() ToastContentBuilder builder = new ToastContentBuilder(); // Include launch string so we know what to open when user clicks toast + builder.AddArgument("action", "viewForecast"); builder.AddArgument("zip", 98008); // We'll always have this summary text on our toast notification diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs index d78b39202d8..67d9c4e29f5 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/App.xaml.cs @@ -50,18 +50,21 @@ private void ToastNotificationManagerCompat_OnActivated(ToastNotificationActivat int conversationId = args.GetInt("conversationId"); // If no specific action, view the conversation - if (!args.TryGetValue("action", out MyToastActions action)) - { - // Make sure we have a window open and in foreground - OpenWindowIfNeeded(); - - // And then show the conversation - (Current.Windows[0] as MainWindow).ShowConversation(conversationId); - } - else + if (args.TryGetValue("action", out MyToastActions action)) { switch (action) { + // View conversation + case MyToastActions.ViewConversation: + + // Make sure we have a window open and in foreground + OpenWindowIfNeeded(); + + // And then show the conversation + (Current.Windows[0] as MainWindow).ShowConversation(conversationId); + + break; + // Open the image case MyToastActions.ViewImage: diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs index 49d9c9227cc..98aff66b8f1 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs @@ -36,6 +36,7 @@ private async void ButtonPopToast_Click(object sender, RoutedEventArgs e) new ToastContentBuilder() // Arguments that are returned when the user clicks the toast or a button + .AddArgument("action", MyToastActions.ViewConversation) .AddArgument("conversationId", conversationId) // Visual content diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs index acdaf7dc5a7..13711b8ffdf 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MyToastActions.cs @@ -6,6 +6,11 @@ namespace Microsoft.Toolkit.Win32.WpfCore.SampleApp { public enum MyToastActions { + /// + /// View the conversation + /// + ViewConversation, + /// /// Inline reply to a message /// From 5c8fc1b97034c6d07a258c302349a5dfa833d0d1 Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Fri, 6 Nov 2020 16:11:15 -0800 Subject: [PATCH 45/46] ToastButton builders --- .../Builder/ToastContentBuilder.Actions.cs | 98 ++---- .../Toasts/Builder/ToastContentBuilder.cs | 36 ++- .../Toasts/IToastActivateableBuilder.cs | 94 ++++++ .../Toasts/ToastButton.cs | 288 +++++++++++++++++- .../SamplePages/Toast/ToastCode.bind | 3 +- .../MainWindow.xaml.cs | 23 +- .../TestToastContentBuilder.cs | 218 +++++++------ 7 files changed, 570 insertions(+), 190 deletions(-) create mode 100644 Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs index 49a8818c549..32e50870076 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.Actions.cs @@ -48,26 +48,6 @@ private IList InputList } } - /// - /// Add a button to the current toast. - /// - /// Text to display on the button. - /// Type of activation this button will use when clicked. Defaults to Foreground. - /// App-defined arguments that the app can later retrieve once it is activated when the user clicks the button. - /// The current instance of -#if WINRT - [Windows.Foundation.Metadata.DefaultOverload] -#endif - public ToastContentBuilder AddButton(string content, ToastActivationType activationType, ToastArguments arguments) - { - AddButton(content, activationType, SerializeArgumentsIncludingGeneric(arguments)); - - // Remove this button from the custom arguments list - _buttonsUsingCustomArguments.RemoveAt(_buttonsUsingCustomArguments.Count - 1); - - return this; - } - private string SerializeArgumentsIncludingGeneric(ToastArguments arguments) { if (_genericArguments.Count == 0) @@ -86,27 +66,6 @@ private string SerializeArgumentsIncludingGeneric(ToastArguments arguments) return arguments.ToString(); } - /// - /// Add a button to the current toast. - /// - /// Text to display on the button. - /// Type of activation this button will use when clicked. Defaults to Foreground. - /// App-defined arguments that the app can later retrieve once it is activated when the user clicks the button. - /// Optional image icon for the button to display (required for buttons adjacent to inputs like quick reply). - /// The current instance of -#if WINRT - [Windows.Foundation.Metadata.DefaultOverload] -#endif - public ToastContentBuilder AddButton(string content, ToastActivationType activationType, ToastArguments arguments, Uri imageUri) - { - AddButton(content, activationType, SerializeArgumentsIncludingGeneric(arguments), imageUri); - - // Remove this button from the custom arguments list - _buttonsUsingCustomArguments.RemoveAt(_buttonsUsingCustomArguments.Count - 1); - - return this; - } - /// /// Add a button to the current toast. /// @@ -119,19 +78,6 @@ public ToastContentBuilder AddButton(string content, ToastActivationType activat return AddButton(content, activationType, arguments, default); } - /// - /// Add an button to the toast that will be display to the right of the input text box, achieving a quick reply scenario. - /// - /// ID of an existing in order to have this button display to the right of the input, achieving a quick reply scenario. - /// Text to display on the button. - /// Type of activation this button will use when clicked. Defaults to Foreground. - /// App-defined arguments that the app can later retrieve once it is activated when the user clicks the button. - /// The current instance of - public ToastContentBuilder AddButton(string textBoxId, string content, ToastActivationType activationType, ToastArguments arguments) - { - return AddButton(textBoxId, content, activationType, arguments, default); - } - /// /// Add a button to the current toast. /// @@ -140,6 +86,9 @@ public ToastContentBuilder AddButton(string textBoxId, string content, ToastActi /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the button. /// Optional image icon for the button to display (required for buttons adjacent to inputs like quick reply). /// The current instance of +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif public ToastContentBuilder AddButton(string content, ToastActivationType activationType, string arguments, Uri imageUri) { // Add new button @@ -156,28 +105,6 @@ public ToastContentBuilder AddButton(string content, ToastActivationType activat return AddButton(button); } - /// - /// Add an button to the toast that will be display to the right of the input text box, achieving a quick reply scenario. - /// - /// ID of an existing in order to have this button display to the right of the input, achieving a quick reply scenario. - /// Text to display on the button. - /// Type of activation this button will use when clicked. Defaults to Foreground. - /// App-defined arguments that the app can later retrieve once it is activated when the user clicks the button. - /// An optional image icon for the button to display (required for buttons adjacent to inputs like quick reply) - /// The current instance of -#if WINRT - [Windows.Foundation.Metadata.DefaultOverload] -#endif - public ToastContentBuilder AddButton(string textBoxId, string content, ToastActivationType activationType, ToastArguments arguments, Uri imageUri) - { - AddButton(textBoxId, content, activationType, SerializeArgumentsIncludingGeneric(arguments), imageUri); - - // Remove this button from the custom arguments list - _buttonsUsingCustomArguments.RemoveAt(_buttonsUsingCustomArguments.Count - 1); - - return this; - } - /// /// Add a button to the current toast. /// @@ -185,19 +112,30 @@ public ToastContentBuilder AddButton(string textBoxId, string content, ToastActi /// The current instance of public ToastContentBuilder AddButton(IToastButton button) { + if (button is ToastButton toastButton && toastButton.Content == null) + { + throw new InvalidOperationException("Content is required on button."); + } + // List has max 5 buttons if (ButtonList.Count == 5) { throw new InvalidOperationException("A toast can't have more than 5 buttons"); } - ButtonList.Add(button); - - if (button is ToastButton b) + if (button is ToastButton b && b.CanAddArguments()) { - _buttonsUsingCustomArguments.Add(b); + foreach (var arg in _genericArguments) + { + if (!b.ContainsArgument(arg.Key)) + { + b.AddArgument(arg.Key, arg.Value); + } + } } + ButtonList.Add(button); + return this; } diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs index be4ed3205c4..0850a1b013d 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/Builder/ToastContentBuilder.cs @@ -11,13 +11,14 @@ namespace Microsoft.Toolkit.Uwp.Notifications /// Builder class used to create /// public partial class ToastContentBuilder +#if !WINRT + : IToastActivateableBuilder +#endif { private Dictionary _genericArguments = new Dictionary(); private bool _customArgumentsUsedOnToastItself; - private List _buttonsUsingCustomArguments = new List(); - /// /// Gets internal instance of . This is equivalent to the call to . /// @@ -196,14 +197,9 @@ private ToastContentBuilder AddArgumentHelper(string key, string value) { foreach (var button in actions.Buttons) { - if (button is ToastButton b && b.ActivationType != ToastActivationType.Protocol && !_buttonsUsingCustomArguments.Contains(b)) + if (button is ToastButton b && b.CanAddArguments() && !b.ContainsArgument(key)) { - var bArgs = ToastArguments.Parse(b.Arguments); - if (!bArgs.Contains(key)) - { - bArgs.Add(key, value); - b.Arguments = bArgs.ToString(); - } + b.AddArgument(key, value); } } } @@ -243,9 +239,31 @@ private string AddArgumentHelper(string existing, string key, string value) /// The protocol to launch. /// The current instance of public ToastContentBuilder SetProtocolActivation(Uri protocol) + { + return SetProtocolActivation(protocol, default); + } + + /// + /// Configures the toast notification to launch the specified url when the toast body is clicked. + /// + /// The protocol to launch. + /// New in Creators Update: The target PFN, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched. + /// The current instance of + public ToastContentBuilder SetProtocolActivation(Uri protocol, string targetApplicationPfn) { Content.Launch = protocol.ToString(); Content.ActivationType = ToastActivationType.Protocol; + + if (targetApplicationPfn != null) + { + if (Content.ActivationOptions == null) + { + Content.ActivationOptions = new ToastActivationOptions(); + } + + Content.ActivationOptions.ProtocolActivationTargetApplicationPfn = targetApplicationPfn; + } + return this; } diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs new file mode 100644 index 00000000000..e44a9261033 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Toolkit.Uwp.Notifications +{ + /// + /// Interfaces for classes that can have activation info added to them. + /// + /// The type of the host object. + internal interface IToastActivateableBuilder + { + /// + /// Adds a key (without value) to the activation arguments that will be returned when the content is clicked. + /// + /// The key. + /// The current instance of the object. + T AddArgument(string key); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] +#endif + T AddArgument(string key, string value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, int value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, double value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, float value); + + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of the object. + T AddArgument(string key, bool value); + +#if !WINRT + /// + /// Adds a key/value to the activation arguments that will be returned when the content is clicked. + /// + /// The key for this value. + /// The value itself. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current instance of the object. + T AddArgument(string key, Enum value); +#endif + + /// + /// Configures the content to use background activation when it is clicked. + /// + /// The current instance of the object. + T SetBackgroundActivation(); + + /// + /// Configures the content to use protocol activation when it is clicked. + /// + /// The protocol to launch. + /// The current instance of the object. + T SetProtocolActivation(Uri protocol); + + /// + /// Configures the content to use protocol activation when it is clicked. + /// + /// The protocol to launch. + /// New in Creators Update: The target PFN, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched. + /// The current instance of the object. + T SetProtocolActivation(Uri protocol, string targetApplicationPfn); + } +} diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs index bc9fd89b265..a564d274984 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/ToastButton.cs @@ -3,14 +3,23 @@ // See the LICENSE file in the project root for more information. using System; +using System.Collections.Generic; namespace Microsoft.Toolkit.Uwp.Notifications { /// /// A button that the user can click on a Toast notification. /// - public sealed class ToastButton : IToastButton + public sealed class ToastButton : +#if !WINRT + IToastActivateableBuilder, +#endif + IToastButton { + private Dictionary _arguments = new Dictionary(); + + private bool _usingCustomArguments; + /// /// Initializes a new instance of the class. /// @@ -30,6 +39,17 @@ public ToastButton(string content, string arguments) Content = content; Arguments = arguments; + + _usingCustomArguments = arguments.Length > 0; + } + + /// + /// Initializes a new instance of the class. + /// + public ToastButton() + { + // Arguments are required (we'll initialize to empty string which is fine). + Arguments = string.Empty; } /// @@ -71,6 +91,272 @@ public ToastButton(string content, string arguments) /// public string HintActionId { get; set; } + /// + /// Sets the text to display on the button. + /// + /// The text to display on the button. + /// The current instance of the . + public ToastButton SetContent(string content) + { + Content = content; + return this; + } + + /// + /// Adds a key (without value) to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key. + /// The current instance of + public ToastButton AddArgument(string key) + { + return AddArgumentHelper(key, null); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [Windows.Foundation.Metadata.DefaultOverload] + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, string value) + { + return AddArgumentHelper(key, value); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, int value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, double value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, float value) + { + return AddArgumentHelper(key, value.ToString()); + } + + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. + /// The current instance of +#if WINRT + [return: System.Runtime.InteropServices.WindowsRuntime.ReturnValueName("ToastButton")] +#endif + public ToastButton AddArgument(string key, bool value) + { + return AddArgumentHelper(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space + } + +#if !WINRT + /// + /// Adds a key/value to the activation arguments that will be returned when the toast notification or its buttons are clicked. + /// + /// The key for this value. + /// The value itself. Note that the enums are stored using their numeric value, so be aware that changing your enum number values might break existing activation of toasts currently in Action Center. + /// The current instance of + public ToastButton AddArgument(string key, Enum value) + { + return AddArgumentHelper(key, ((int)(object)value).ToString()); + } +#endif + + private ToastButton AddArgumentHelper(string key, string value) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + if (_usingCustomArguments) + { + throw new InvalidOperationException("You cannot use the AddArgument methods if you've set the Arguments property. Use the default ToastButton constructor instead."); + } + + if (ActivationType == ToastActivationType.Protocol) + { + throw new InvalidOperationException("You cannot use the AddArgument methods when using protocol activation."); + } + + bool alreadyExists = _arguments.ContainsKey(key); + + _arguments[key] = value; + + Arguments = alreadyExists ? SerializeArgumentsHelper(_arguments) : AddArgumentHelper(Arguments, key, value); + + return this; + } + + private string SerializeArgumentsHelper(IDictionary arguments) + { + var args = new ToastArguments(); + + foreach (var a in arguments) + { + args.Add(a.Key, a.Value); + } + + return args.ToString(); + } + + private string AddArgumentHelper(string existing, string key, string value) + { + string pair = ToastArguments.EncodePair(key, value); + + if (string.IsNullOrEmpty(existing)) + { + return pair; + } + else + { + return existing + ToastArguments.Separator + pair; + } + } + + /// + /// Configures the button to launch the specified url when the button is clicked. + /// + /// The protocol to launch. + /// The current instance of + public ToastButton SetProtocolActivation(Uri protocol) + { + return SetProtocolActivation(protocol, default); + } + + /// + /// Configures the button to launch the specified url when the button is clicked. + /// + /// The protocol to launch. + /// New in Creators Update: The target PFN, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched. + /// The current instance of + public ToastButton SetProtocolActivation(Uri protocol, string targetApplicationPfn) + { + if (_arguments.Count > 0) + { + throw new InvalidOperationException("SetProtocolActivation cannot be used in conjunction with AddArgument"); + } + + Arguments = protocol.ToString(); + ActivationType = ToastActivationType.Protocol; + + if (targetApplicationPfn != null) + { + if (ActivationOptions == null) + { + ActivationOptions = new ToastActivationOptions(); + } + + ActivationOptions.ProtocolActivationTargetApplicationPfn = targetApplicationPfn; + } + + return this; + } + + /// + /// Configures the button to use background activation when the button is clicked. + /// + /// The current instance of + public ToastButton SetBackgroundActivation() + { + ActivationType = ToastActivationType.Background; + return this; + } + + /// + /// Sets the behavior that the toast should use when the user invokes this button. Desktop-only, supported in builds 16251 or higher. New in Fall Creators Update. + /// + /// The behavior that the toast should use when the user invokes this button. + /// The current instance of + public ToastButton SetAfterActivationBehavior(ToastAfterActivationBehavior afterActivationBehavior) + { + if (ActivationOptions == null) + { + ActivationOptions = new ToastActivationOptions(); + } + + ActivationOptions.AfterActivationBehavior = afterActivationBehavior; + + return this; + } + + /// + /// Sets an identifier used in telemetry to identify your category of action. This should be something like "Delete", "Reply", or "Archive". In the upcoming toast telemetry dashboard in Dev Center, you will be able to view how frequently your actions are being clicked. + /// + /// An identifier used in telemetry to identify your category of action. + /// The current instance of + public ToastButton SetHintActionId(string actionId) + { + HintActionId = actionId; + return this; + } + + /// + /// Sets an optional image icon for the button to display (required for buttons adjacent to inputs like quick reply). + /// + /// An optional image icon for the button to display. + /// The current instance of + public ToastButton SetImageUri(Uri imageUri) + { + ImageUri = imageUri.ToString(); + return this; + } + + /// + /// Sets the ID of an existing in order to have this button display to the right of the input, achieving a quick reply scenario. + /// + /// The ID of an existing . + /// The current instance of + public ToastButton SetTextBoxId(string textBoxId) + { + TextBoxId = textBoxId; + return this; + } + + internal bool CanAddArguments() + { + return ActivationType != ToastActivationType.Protocol && !_usingCustomArguments; + } + + internal bool ContainsArgument(string key) + { + return _arguments.ContainsKey(key); + } + internal Element_ToastAction ConvertToElement() { var el = new Element_ToastAction() diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind index 9d72fed3c34..7d5a3bafa44 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/Toast/ToastCode.bind @@ -1,7 +1,8 @@ private void PopToast() { // Generate the toast notification content and pop the toast - new ToastContentBuilder().SetToastScenario(ToastScenario.Reminder) + new ToastContentBuilder() + .SetToastScenario(ToastScenario.Reminder) .AddArgument("action", "viewEvent") .AddArgument("eventId", 1983) .AddText("Adaptive Tiles Meeting") diff --git a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs index 98aff66b8f1..9f106fb59c7 100644 --- a/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs +++ b/Microsoft.Toolkit.Win32.WpfCore.SampleApp/MainWindow.xaml.cs @@ -49,15 +49,20 @@ private async void ButtonPopToast_Click(object sender, RoutedEventArgs e) .AddInputTextBox("tbReply", "Type a reply") // Buttons - .AddButton("Reply", ToastActivationType.Background, new ToastArguments() - .Add("action", MyToastActions.Reply)) - - .AddButton("Like", ToastActivationType.Background, new ToastArguments() - .Add("action", MyToastActions.Like)) - - .AddButton("View", ToastActivationType.Foreground, new ToastArguments() - .Add("action", MyToastActions.ViewImage) - .Add("imageUrl", image)) + .AddButton(new ToastButton() + .SetContent("Reply") + .AddArgument("action", MyToastActions.Reply) + .SetBackgroundActivation()) + + .AddButton(new ToastButton() + .SetContent("Like") + .AddArgument("action", MyToastActions.Like) + .SetBackgroundActivation()) + + .AddButton(new ToastButton() + .SetContent("View") + .AddArgument("action", MyToastActions.ViewImage) + .AddArgument("imageUrl", image)) // And show the toast! .Show(); diff --git a/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs b/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs index 7a57a850214..74af322dee5 100644 --- a/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs +++ b/UnitTests/UnitTests.Notifications.Shared/TestToastContentBuilder.cs @@ -150,17 +150,23 @@ public void AddArgumentTest_Generic_ReturnSelfWithArgumentsAdded() // Act ToastContentBuilder builder = new ToastContentBuilder(); ToastContentBuilder anotherReference = builder - .AddButton("Accept", ToastActivationType.Background, new ToastArguments() - .Add("action", "accept")) + .AddButton(new ToastButton() + .SetContent("Accept") + .AddArgument("action", "accept") + .SetBackgroundActivation()) .AddButton(new ToastButtonSnooze()) .AddButton("View", ToastActivationType.Protocol, "https://msn.com") // Add generic arguments halfway through (should be applied to existing buttons and to any subsequent buttons added later) .AddArgument(userIdKey, userIdValue) - .AddButton("Decline", ToastActivationType.Background, new ToastArguments() - .Add("action", "decline")) - .AddButton("Report", ToastActivationType.Protocol, "https://microsoft.com"); + .AddButton(new ToastButton() + .SetContent("Decline") + .AddArgument("action", "decline") + .SetBackgroundActivation()) + .AddButton(new ToastButton() + .SetContent("Report") + .SetProtocolActivation(new Uri("https://microsoft.com"))); // Assert Assert.AreSame(builder, anotherReference); @@ -188,7 +194,7 @@ public void AddArgumentTest_Generic_ReturnSelfWithArgumentsAdded() var button5 = actions.Buttons[4] as ToastButton; Assert.AreEqual("Report", button5.Content); - Assert.AreEqual("https://microsoft.com", button5.Arguments); + Assert.AreEqual("https://microsoft.com/", button5.Arguments); } [TestMethod] @@ -197,16 +203,20 @@ public void AddArgumentTest_ReplacingWithinButton_ReturnSelfWithArgumentsAdded() // Act ToastContentBuilder builder = new ToastContentBuilder(); ToastContentBuilder anotherReference = builder - .AddButton("Accept", ToastActivationType.Background, new ToastArguments() - .Add("action", "accept") - .Add("userId", 601)) + .AddButton(new ToastButton() + .SetContent("Accept") + .AddArgument("action", "accept") + .AddArgument("userId", 601) + .SetBackgroundActivation()) - // Add generic arguments halfway through (should be applied to existing buttons and to any subsequent buttons added later) + // Add generic arguments halfway through (in this case shouldn't overwrite anything) .AddArgument("userId", 542) - .AddButton("Decline", ToastActivationType.Background, new ToastArguments() - .Add("action", "decline") - .Add("userId", 601)); + .AddButton(new ToastButton() + .SetContent("Decline") + .AddArgument("action", "decline") + .AddArgument("userId", 601) + .SetBackgroundActivation()); // Assert Assert.AreSame(builder, anotherReference); @@ -241,8 +251,9 @@ public void AddArgumentTest_AvoidModifyingCustomButtons_ReturnSelfWithArgumentsA .AddButton("Decline", ToastActivationType.Background, "myDeclineStr") - .AddButton("View", ToastActivationType.Foreground, new ToastArguments() - .Add("action", "view")); + .AddButton(new ToastButton() + .SetContent("View") + .AddArgument("action", "view")); // Assert Assert.AreSame(builder, anotherReference); @@ -287,8 +298,10 @@ public void SetProtocolActivationTest_ReturnSelfWithArgumentsAdded() // Act ToastContentBuilder builder = new ToastContentBuilder(); ToastContentBuilder anotherReference = builder - .AddButton("Accept", ToastActivationType.Background, new ToastArguments() - .Add("action", "accept")) + .AddButton(new ToastButton() + .SetContent("Accept") + .AddArgument("action", "accept") + .SetBackgroundActivation()) .AddArgument("userId", 542) @@ -296,8 +309,10 @@ public void SetProtocolActivationTest_ReturnSelfWithArgumentsAdded() .AddArgument("name", "Andrew") - .AddButton("Decline", ToastActivationType.Background, new ToastArguments() - .Add("action", "decline")); + .AddButton(new ToastButton() + .SetContent("Decline") + .AddArgument("action", "decline") + .SetBackgroundActivation()); // Assert Assert.AreSame(builder, anotherReference); @@ -315,6 +330,100 @@ public void SetProtocolActivationTest_ReturnSelfWithArgumentsAdded() Assert.AreEqual("action=decline;userId=542;name=Andrew", button2.Arguments); } + [TestMethod] + public void ToastButtonBuilders_General_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .AddArgument("action", "view") + .AddArgument("imageId", 601); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("action=view;imageId=601", button.Arguments); + Assert.AreEqual(ToastActivationType.Foreground, button.ActivationType); + } + + [TestMethod] + public void ToastButtonBuilders_AllProperties_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .SetImageUri(new Uri("ms-appx:///Assets/view.png")) + .AddArgument("action", "view") + .SetBackgroundActivation() + .SetAfterActivationBehavior(ToastAfterActivationBehavior.PendingUpdate) + .SetHintActionId("viewImage"); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("action=view", button.Arguments); + Assert.AreEqual("ms-appx:///Assets/view.png", button.ImageUri); + Assert.AreEqual(ToastActivationType.Background, button.ActivationType); + Assert.AreEqual(ToastAfterActivationBehavior.PendingUpdate, button.ActivationOptions.AfterActivationBehavior); + Assert.AreEqual("viewImage", button.HintActionId); + } + + [TestMethod] + public void ToastButtonBuilders_ProtocolActivation_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .SetProtocolActivation(new Uri("https://msn.com")); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("https://msn.com/", button.Arguments); + Assert.AreEqual(ToastActivationType.Protocol, button.ActivationType); + } + + [TestMethod] + public void ToastButtonBuilders_ProtocolActivationWithPfn_ReturnSelf() + { + ToastButton button = new ToastButton(); + ToastButton anotherReference = button + .SetContent("View") + .SetProtocolActivation(new Uri("https://msn.com"), "MyPfn"); + + Assert.AreSame(button, anotherReference); + Assert.AreEqual("View", button.Content); + Assert.AreEqual("https://msn.com/", button.Arguments); + Assert.AreEqual(ToastActivationType.Protocol, button.ActivationType); + Assert.AreEqual("MyPfn", button.ActivationOptions.ProtocolActivationTargetApplicationPfn); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidProtocolAfterArguments_ReturnSelf() + { + new ToastButton() + .SetContent("View") + .AddArgument("action", "view") + .SetProtocolActivation(new Uri("https://msn.com")); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidArgumentsAfterProtocol_ReturnSelf() + { + new ToastButton() + .SetContent("View") + .SetProtocolActivation(new Uri("https://msn.com")) + .AddArgument("action", "view"); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void ToastButtonBuilders_InvalidArgumentsAfterCustomArguments_ReturnSelf() + { + var button = new ToastButton("View", "viewArgs"); + + button.AddArgument("action", "view"); + } + [TestMethod] public void SetToastDurationTest_WithCustomToastDuration_ReturnSelfWithCustomToastDurationSet() { @@ -780,27 +889,6 @@ public void AddButtonTest_WithTextOnlyButton_ReturnSelfWithButtonAdded() Assert.AreEqual(testButtonLaunchArgs, button.Arguments); } - [TestMethod] - public void AddButtonTest_WithTextOnlyButtonAndToastArguments_ReturnSelfWithButtonAdded() - { - // Arrange - string testButtonContent = "Test Button Content"; - ToastActivationType testToastActivationType = ToastActivationType.Background; - var testButtonLaunchArgs = new ToastArguments().Add("action", "view"); - ToastContentBuilder builder = new ToastContentBuilder(); - - // Act - ToastContentBuilder anotherReference = builder.AddButton(testButtonContent, testToastActivationType, testButtonLaunchArgs); - - // Assert - Assert.AreSame(builder, anotherReference); - - var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; - Assert.AreEqual(testButtonContent, button.Content); - Assert.AreEqual(testToastActivationType, button.ActivationType); - Assert.AreEqual(testButtonLaunchArgs.ToString(), button.Arguments); - } - [TestMethod] public void AddButtonTest_WithCustomImageAndTextButton_ReturnSelfWithButtonAdded() { @@ -825,30 +913,6 @@ public void AddButtonTest_WithCustomImageAndTextButton_ReturnSelfWithButtonAdded Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); } - [TestMethod] - public void AddButtonTest_WithCustomImageAndTextButtonAndToastArguments_ReturnSelfWithButtonAdded() - { - // Arrange - string testButtonContent = "Test Button Content"; - ToastActivationType testToastActivationType = ToastActivationType.Background; - var testButtonLaunchArgs = new ToastArguments().Add("action", "accept"); - Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); - - ToastContentBuilder builder = new ToastContentBuilder(); - - // Act - ToastContentBuilder anotherReference = builder.AddButton(testButtonContent, testToastActivationType, testButtonLaunchArgs, testImageUriSrc); - - // Assert - Assert.AreSame(builder, anotherReference); - - var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; - Assert.AreEqual(testButtonContent, button.Content); - Assert.AreEqual(testToastActivationType, button.ActivationType); - Assert.AreEqual(testButtonLaunchArgs.ToString(), button.Arguments); - Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); - } - [TestMethod] public void AddButtonTest_WithTextBoxId_ReturnSelfWithButtonAdded() { @@ -875,32 +939,6 @@ public void AddButtonTest_WithTextBoxId_ReturnSelfWithButtonAdded() Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); } - [TestMethod] - public void AddButtonTest_WithTextBoxIdAndToastArguments_ReturnSelfWithButtonAdded() - { - // Arrange - string testInputTextBoxId = Guid.NewGuid().ToString(); - string testButtonContent = "Test Button Content"; - ToastActivationType testToastActivationType = ToastActivationType.Background; - var testButtonLaunchArgs = new ToastArguments().Add("action", "send"); - Uri testImageUriSrc = new Uri("C:/justatesturi.jpg"); - - ToastContentBuilder builder = new ToastContentBuilder(); - - // Act - ToastContentBuilder anotherReference = builder.AddButton(testInputTextBoxId, testButtonContent, testToastActivationType, testButtonLaunchArgs, testImageUriSrc); - - // Assert - Assert.AreSame(builder, anotherReference); - - var button = (builder.Content.Actions as ToastActionsCustom).Buttons.First() as ToastButton; - Assert.AreEqual(testInputTextBoxId, button.TextBoxId); - Assert.AreEqual(testButtonContent, button.Content); - Assert.AreEqual(testToastActivationType, button.ActivationType); - Assert.AreEqual(testButtonLaunchArgs.ToString(), button.Arguments); - Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri); - } - [TestMethod] public void AddInputTextBoxTest_WithStringIdOnly_ReturnSelfWithInputTextBoxAdded() { From 0f1bdb41415b27f37c77cae0c1d1f6d90a9961de Mon Sep 17 00:00:00 2001 From: Andrew Leader Date: Fri, 6 Nov 2020 16:22:15 -0800 Subject: [PATCH 46/46] Added missing header --- .../Toasts/IToastActivateableBuilder.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs b/Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs index e44a9261033..6e62a0a7e8b 100644 --- a/Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs +++ b/Microsoft.Toolkit.Uwp.Notifications/Toasts/IToastActivateableBuilder.cs @@ -1,6 +1,8 @@ -using System; -using System.Collections.Generic; -using System.Text; +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; namespace Microsoft.Toolkit.Uwp.Notifications {