diff --git a/components/Notifications/OpenSolution.bat b/components/Notifications/OpenSolution.bat
new file mode 100644
index 000000000..814a56d4b
--- /dev/null
+++ b/components/Notifications/OpenSolution.bat
@@ -0,0 +1,3 @@
+@ECHO OFF
+
+powershell ..\..\tooling\ProjectHeads\GenerateSingleSampleHeads.ps1 -componentPath %CD% %*
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveGroup.cs b/components/Notifications/src/Adaptive/AdaptiveGroup.cs
new file mode 100644
index 000000000..e4f6310cc
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveGroup.cs
@@ -0,0 +1,41 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Groups semantically identify that the content in the group must either be displayed as a whole, or not displayed if it cannot fit. Groups also allow creating multiple columns. Supported on Tiles since RTM. Supported on Toasts since Anniversary Update.
+ ///
+ public sealed class AdaptiveGroup : ITileBindingContentAdaptiveChild, IAdaptiveChild, IToastBindingGenericChild
+ {
+ ///
+ /// Gets the only valid children of groups are .
+ /// Each subgroup is displayed as a separate vertical column. Note that you must
+ /// include at least one subgroup in your group, otherwise an
+ /// will be thrown when you try to retrieve the XML for the notification.
+ ///
+ public IList Children { get; private set; } = new List();
+
+ internal Element_AdaptiveGroup ConvertToElement()
+ {
+ if (Children.Count == 0)
+ {
+ throw new InvalidOperationException("Groups must have at least one child subgroup. The Children property had zero items in it.");
+ }
+
+ Element_AdaptiveGroup group = new Element_AdaptiveGroup();
+
+ foreach (var subgroup in Children)
+ {
+ group.Children.Add(subgroup.ConvertToElement());
+ }
+
+ return group;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveHelper.cs b/components/Notifications/src/Adaptive/AdaptiveHelper.cs
new file mode 100644
index 000000000..67b4db649
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveHelper.cs
@@ -0,0 +1,41 @@
+// 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 CommunityToolkit.Notifications.Adaptive
+{
+ internal static class AdaptiveHelper
+ {
+ internal static object ConvertToElement(object obj)
+ {
+ if (obj is AdaptiveText)
+ {
+ return (obj as AdaptiveText).ConvertToElement();
+ }
+
+ if (obj is AdaptiveImage)
+ {
+ return (obj as AdaptiveImage).ConvertToElement();
+ }
+
+ if (obj is AdaptiveGroup)
+ {
+ return (obj as AdaptiveGroup).ConvertToElement();
+ }
+
+ if (obj is AdaptiveSubgroup)
+ {
+ return (obj as AdaptiveSubgroup).ConvertToElement();
+ }
+
+ if (obj is AdaptiveProgressBar)
+ {
+ return (obj as AdaptiveProgressBar).ConvertToElement();
+ }
+
+ throw new NotImplementedException("Unknown object: " + obj.GetType());
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveImage.cs b/components/Notifications/src/Adaptive/AdaptiveImage.cs
new file mode 100644
index 000000000..2a5a756f2
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveImage.cs
@@ -0,0 +1,92 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// An inline image.
+ ///
+ public sealed class AdaptiveImage
+ : IBaseImage,
+ IToastBindingGenericChild,
+ ITileBindingContentAdaptiveChild,
+ IAdaptiveChild,
+ IAdaptiveSubgroupChild
+ {
+ ///
+ /// Gets or sets the desired cropping of the image.
+ /// Supported on Tiles since RTM. Supported on Toast since Anniversary Update.
+ ///
+ public AdaptiveImageCrop HintCrop { get; set; }
+
+ ///
+ /// Gets or sets a value whether a margin is removed. images have an 8px margin around them.
+ /// You can remove this margin by setting this property to true.
+ /// Supported on Tiles since RTM. Supported on Toast since Anniversary Update.
+ ///
+ public bool? HintRemoveMargin { get; set; }
+
+ ///
+ /// Gets or sets the horizontal alignment of the image.
+ /// For Toast, this is only supported when inside an .
+ ///
+ public AdaptiveImageAlign HintAlign { get; set; }
+
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the image (Required).
+ /// Can be from your application package, application data, or the internet.
+ /// Internet images must be less than 200 KB in size.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ public string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets set to true to allow Windows to append a query string to the image URI
+ /// supplied in the Tile notification. Use this attribute if your server hosts
+ /// images and can handle query strings, either by retrieving an image variant based
+ /// on the query strings or by ignoring the query string and returning the image
+ /// as specified without the query string. This query string specifies scale,
+ /// contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ ///
+ /// Returns the image's source string.
+ ///
+ /// The image's source string.
+ public override string ToString()
+ {
+ if (Source == null)
+ {
+ return "Source is null";
+ }
+
+ return Source;
+ }
+
+ internal Element_AdaptiveImage ConvertToElement()
+ {
+ Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this);
+
+ image.Crop = HintCrop;
+ image.RemoveMargin = HintRemoveMargin;
+ image.Align = HintAlign;
+ image.Placement = AdaptiveImagePlacement.Inline;
+
+ return image;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveImageEnums.cs b/components/Notifications/src/Adaptive/AdaptiveImageEnums.cs
new file mode 100644
index 000000000..498471c97
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveImageEnums.cs
@@ -0,0 +1,58 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Specifies the horizontal alignment for an image.
+ ///
+ public enum AdaptiveImageAlign
+ {
+ ///
+ /// Default value, alignment behavior determined by renderer.
+ ///
+ Default,
+
+ ///
+ /// Image stretches to fill available width (and potentially available height too, depending on where the image is).
+ ///
+ Stretch,
+
+ ///
+ /// Align the image to the left, displaying the image at its native resolution.
+ ///
+ Left,
+
+ ///
+ /// Align the image in the center horizontally, displaying the image at its native resolution.
+ ///
+ Center,
+
+ ///
+ /// Align the image to the right, displaying the image at its native resolution.
+ ///
+ Right
+ }
+
+ ///
+ /// Specify the desired cropping of the image.
+ ///
+ public enum AdaptiveImageCrop
+ {
+ ///
+ /// Default value, cropping behavior determined by renderer.
+ ///
+ Default,
+
+ ///
+ /// Image is not cropped.
+ ///
+ None,
+
+ ///
+ /// Image is cropped to a circle shape.
+ ///
+ Circle
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveProgressBar.cs b/components/Notifications/src/Adaptive/AdaptiveProgressBar.cs
new file mode 100644
index 000000000..7f39bbceb
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveProgressBar.cs
@@ -0,0 +1,63 @@
+// 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.
+
+#pragma warning disable SA1121 // UseBuiltInTypeAlias
+
+using System;
+using CommunityToolkit.Notifications.Adaptive.Elements;
+using BindableProgressBarValue = CommunityToolkit.Notifications.BindableProgressBarValue;
+using BindableString = CommunityToolkit.Notifications.BindableString;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// New in Creators Update: A progress bar. Only supported on toasts on Desktop, build 15007 or newer.
+ ///
+ public sealed class AdaptiveProgressBar : IToastBindingGenericChild
+ {
+ ///
+ /// Gets or sets an optional title string. Supports data binding.
+ ///
+ public BindableString Title { get; set; }
+
+ ///
+ /// Gets or sets the value of the progress bar. Supports data binding. Defaults to 0.
+ ///
+ public BindableProgressBarValue Value { get; set; } = AdaptiveProgressBarValue.FromValue(0);
+
+ ///
+ /// Gets or sets an optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed.
+ ///
+ public BindableString ValueStringOverride { get; set; }
+
+ ///
+ /// Gets or sets a status string (Required), which is displayed underneath the progress bar. This string should reflect the status of the operation, like "Downloading..." or "Installing..."
+ ///
+ public BindableString Status { get; set; }
+
+ internal Element_AdaptiveProgressBar ConvertToElement()
+ {
+ // If Value not provided, we use 0
+ var val = Value;
+ if (val == null)
+ {
+ val = AdaptiveProgressBarValue.FromValue(0);
+ }
+
+ var answer = new Element_AdaptiveProgressBar();
+
+ answer.Title = Title?.ToXmlString();
+ answer.Value = val.ToXmlString();
+ answer.ValueStringOverride = ValueStringOverride?.ToXmlString();
+ answer.Status = Status?.ToXmlString();
+
+ if (answer.Status == null)
+ {
+ throw new NullReferenceException("Status property is required.");
+ }
+
+ return answer;
+ }
+ }
+}
diff --git a/components/Notifications/src/Adaptive/AdaptiveProgressBarValue.cs b/components/Notifications/src/Adaptive/AdaptiveProgressBarValue.cs
new file mode 100644
index 000000000..a6e6880cd
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveProgressBarValue.cs
@@ -0,0 +1,96 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A class that represents the progress bar's value.
+ ///
+ public sealed class AdaptiveProgressBarValue
+ {
+ ///
+ /// Gets or sets the property name to bind to.
+ ///
+ public string BindingName { get; set; }
+
+ ///
+ /// Gets or sets the value (0-1) representing the percent complete.
+ ///
+ public double Value { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether the progress bar is indeterminate.
+ ///
+ public bool IsIndeterminate { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ private AdaptiveProgressBarValue()
+ {
+ }
+
+ internal string ToXmlString()
+ {
+ if (IsIndeterminate)
+ {
+ return "indeterminate";
+ }
+
+ if (BindingName != null)
+ {
+ return "{" + BindingName + "}";
+ }
+
+ return Value.ToString();
+ }
+
+ ///
+ /// Gets an indeterminate progress bar value.
+ ///
+ public static AdaptiveProgressBarValue Indeterminate
+ {
+ get
+ {
+ return new AdaptiveProgressBarValue()
+ {
+ IsIndeterminate = true
+ };
+ }
+ }
+
+ ///
+ /// Returns a progress bar value using the specified value (0-1) representing the percent complete.
+ ///
+ /// The value, 0-1, inclusive.
+ /// A progress bar value.
+ public static AdaptiveProgressBarValue FromValue(double d)
+ {
+ if (d < 0 || d > 1)
+ {
+ throw new ArgumentOutOfRangeException("d", "Value must be between 0 and 1, inclusive.");
+ }
+
+ return new AdaptiveProgressBarValue()
+ {
+ Value = d
+ };
+ }
+
+ ///
+ /// Returns a progress bar value using the specified binding name.
+ ///
+ /// The property to bind to.
+ /// A progress bar value.
+ public static AdaptiveProgressBarValue FromBinding(string bindingName)
+ {
+ return new AdaptiveProgressBarValue()
+ {
+ BindingName = bindingName
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveSubgroup.cs b/components/Notifications/src/Adaptive/AdaptiveSubgroup.cs
new file mode 100644
index 000000000..9a136c843
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveSubgroup.cs
@@ -0,0 +1,77 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Subgroups are vertical columns that can contain text and images. Supported on Tiles since RTM. Supported on Toasts since Anniversary Update.
+ ///
+ public sealed class AdaptiveSubgroup
+ {
+ ///
+ /// Gets a list of Children. and are valid children of subgroups.
+ ///
+ public IList Children { get; private set; } = new List();
+
+ private int? _hintWeight;
+
+ ///
+ /// Gets or sets the width of this subgroup column by specifying the weight, relative to the other subgroups.
+ ///
+ public int? HintWeight
+ {
+ get
+ {
+ return _hintWeight;
+ }
+
+ set
+ {
+ Element_AdaptiveSubgroup.CheckWeight(value);
+
+ _hintWeight = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the vertical alignment of this subgroup's content.
+ ///
+ public AdaptiveSubgroupTextStacking HintTextStacking { get; set; } = Element_AdaptiveSubgroup.DEFAULT_TEXT_STACKING;
+
+ internal Element_AdaptiveSubgroup ConvertToElement()
+ {
+ var subgroup = new Element_AdaptiveSubgroup()
+ {
+ Weight = HintWeight,
+ TextStacking = HintTextStacking
+ };
+
+ foreach (var child in Children)
+ {
+ subgroup.Children.Add(ConvertToSubgroupChildElement(child));
+ }
+
+ return subgroup;
+ }
+
+ private static IElement_AdaptiveSubgroupChild ConvertToSubgroupChildElement(IAdaptiveSubgroupChild child)
+ {
+ if (child is AdaptiveText)
+ {
+ return (child as AdaptiveText).ConvertToElement();
+ }
+
+ if (child is AdaptiveImage)
+ {
+ return (child as AdaptiveImage).ConvertToElement();
+ }
+
+ throw new NotImplementedException("Unknown child: " + child.GetType());
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveSubgroupEnums.cs b/components/Notifications/src/Adaptive/AdaptiveSubgroupEnums.cs
new file mode 100644
index 000000000..ce41b799c
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveSubgroupEnums.cs
@@ -0,0 +1,32 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// TextStacking specifies the vertical alignment of content.
+ ///
+ public enum AdaptiveSubgroupTextStacking
+ {
+ ///
+ /// Renderer automatically selects the default vertical alignment.
+ ///
+ Default,
+
+ ///
+ /// Vertical align to the top.
+ ///
+ Top,
+
+ ///
+ /// Vertical align to the center.
+ ///
+ Center,
+
+ ///
+ /// Vertical align to the bottom.
+ ///
+ Bottom
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/AdaptiveText.cs b/components/Notifications/src/Adaptive/AdaptiveText.cs
new file mode 100644
index 000000000..68775c9c0
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveText.cs
@@ -0,0 +1,133 @@
+// 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.
+
+#pragma warning disable SA1121 // UseBuiltInTypeAlias
+
+using System;
+using CommunityToolkit.Notifications.Adaptive.Elements;
+using BindableString = CommunityToolkit.Notifications.BindableString;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// An adaptive text element.
+ ///
+ public sealed class AdaptiveText
+ : IAdaptiveChild,
+ IAdaptiveSubgroupChild,
+ ITileBindingContentAdaptiveChild,
+ IToastBindingGenericChild
+ {
+ ///
+ /// Gets or sets the text to display. Data binding support added in Creators Update,
+ /// only works for toast top-level text elements.
+ ///
+ public BindableString Text { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags
+ /// such as "en-US" or "fr-FR". The locale specified here overrides any other specified
+ /// locale, such as that in binding or visual. If this value is a literal string,
+ /// this attribute defaults to the user's UI language. If this value is a string reference,
+ /// this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Gets or sets the style that controls the text's font size, weight, and opacity.
+ /// Note that for Toast, the style will only take effect if the text is inside an .
+ ///
+ public AdaptiveTextStyle HintStyle { get; set; }
+
+ ///
+ /// Gets or sets a value whether text wrapping is enabled. For Tiles, this is false by default.
+ /// For Toasts, this is true on top-level text elements, and false inside an .
+ /// Note that for Toast, setting wrap will only take effect if the text is inside an
+ /// (you can use HintMaxLines = 1 to prevent top-level text elements from wrapping).
+ ///
+ public bool? HintWrap { get; set; }
+
+ private int? _hintMaxLines;
+
+ ///
+ /// Gets or sets the maximum number of lines the text element is allowed to display.
+ /// For Tiles, this is infinity by default. For Toasts, top-level text elements will
+ /// have varying max line amounts (and in the Anniversary Update you can change the max lines).
+ /// Text on a Toast inside an will behave identically to Tiles (default to infinity).
+ ///
+ public int? HintMaxLines
+ {
+ get
+ {
+ return _hintMaxLines;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ Element_AdaptiveText.CheckMaxLinesValue(value.Value);
+ }
+
+ _hintMaxLines = value;
+ }
+ }
+
+ private int? _hintMinLines;
+
+ ///
+ /// Gets or sets the minimum number of lines the text element must display.
+ /// Note that for Toast, this property will only take effect if the text is inside an .
+ ///
+ public int? HintMinLines
+ {
+ get
+ {
+ return _hintMinLines;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ Element_AdaptiveText.CheckMinLinesValue(value.Value);
+ }
+
+ _hintMinLines = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the horizontal alignment of the text. Note that for Toast, this property will
+ /// only take effect if the text is inside an .
+ ///
+ public AdaptiveTextAlign HintAlign { get; set; }
+
+ internal Element_AdaptiveText ConvertToElement()
+ {
+ var answer = new Element_AdaptiveText()
+ {
+ Lang = Language,
+ Style = HintStyle,
+ Wrap = HintWrap,
+ MaxLines = HintMaxLines,
+ MinLines = HintMinLines,
+ Align = HintAlign
+ };
+
+ answer.Text = Text?.ToXmlString();
+
+ return answer;
+ }
+
+ ///
+ /// Returns the value of the Text property.
+ ///
+ /// The value of the Text property.
+ public override string ToString()
+ {
+ return Text;
+ }
+ }
+}
diff --git a/components/Notifications/src/Adaptive/AdaptiveTextEnums.cs b/components/Notifications/src/Adaptive/AdaptiveTextEnums.cs
new file mode 100644
index 000000000..60eb0d72e
--- /dev/null
+++ b/components/Notifications/src/Adaptive/AdaptiveTextEnums.cs
@@ -0,0 +1,142 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Text style controls font size, weight, and opacity.
+ ///
+ public enum AdaptiveTextStyle
+ {
+ ///
+ /// Style is determined by the renderer.
+ ///
+ Default,
+
+ ///
+ /// Default value. Paragraph font size, normal weight and opacity.
+ ///
+ Caption,
+
+ ///
+ /// Same as Caption but with subtle opacity.
+ ///
+ CaptionSubtle,
+
+ ///
+ /// H5 font size.
+ ///
+ Body,
+
+ ///
+ /// Same as Body but with subtle opacity.
+ ///
+ BodySubtle,
+
+ ///
+ /// H5 font size, bold weight. Essentially the bold version of Body.
+ ///
+ Base,
+
+ ///
+ /// Same as Base but with subtle opacity.
+ ///
+ BaseSubtle,
+
+ ///
+ /// H4 font size.
+ ///
+ Subtitle,
+
+ ///
+ /// Same as Subtitle but with subtle opacity.
+ ///
+ SubtitleSubtle,
+
+ ///
+ /// H3 font size.
+ ///
+ Title,
+
+ ///
+ /// Same as Title but with subtle opacity.
+ ///
+ TitleSubtle,
+
+ ///
+ /// Same as Title but with top/bottom padding removed.
+ ///
+ TitleNumeral,
+
+ ///
+ /// H2 font size.
+ ///
+ Subheader,
+
+ ///
+ /// Same as Subheader but with subtle opacity.
+ ///
+ SubheaderSubtle,
+
+ ///
+ /// Same as Subheader but with top/bottom padding removed.
+ ///
+ SubheaderNumeral,
+
+ ///
+ /// H1 font size.
+ ///
+ Header,
+
+ ///
+ /// Same as Header but with subtle opacity.
+ ///
+ HeaderSubtle,
+
+ ///
+ /// Same as Header but with top/bottom padding removed.
+ ///
+ HeaderNumeral
+ }
+
+ ///
+ /// Controls the horizontal alignment of text.
+ ///
+ public enum AdaptiveTextAlign
+ {
+ ///
+ /// Alignment is automatically determined by
+ ///
+ Default,
+
+ ///
+ /// The system automatically decides the alignment based on the language and culture.
+ ///
+ Auto,
+
+ ///
+ /// Horizontally align the text to the left.
+ ///
+ Left,
+
+ ///
+ /// Horizontally align the text in the center.
+ ///
+ Center,
+
+ ///
+ /// Horizontally align the text to the right.
+ ///
+ Right
+ }
+
+ internal enum AdaptiveTextPlacement
+ {
+ ///
+ /// Default value
+ ///
+ Inline,
+ Attribution
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/BaseImageHelper.cs b/components/Notifications/src/Adaptive/BaseImageHelper.cs
new file mode 100644
index 000000000..778aedbb1
--- /dev/null
+++ b/components/Notifications/src/Adaptive/BaseImageHelper.cs
@@ -0,0 +1,37 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ internal static class BaseImageHelper
+ {
+ internal static void SetSource(ref string destination, string value)
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(nameof(value));
+ }
+
+ destination = value;
+ }
+
+ internal static Element_AdaptiveImage CreateBaseElement(IBaseImage current)
+ {
+ if (current.Source == null)
+ {
+ throw new NullReferenceException("Source property is required.");
+ }
+
+ return new Element_AdaptiveImage()
+ {
+ Src = current.Source,
+ Alt = current.AlternateText,
+ AddImageQuery = current.AddImageQuery
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/BaseTextHelper.cs b/components/Notifications/src/Adaptive/BaseTextHelper.cs
new file mode 100644
index 000000000..a65f43d93
--- /dev/null
+++ b/components/Notifications/src/Adaptive/BaseTextHelper.cs
@@ -0,0 +1,20 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ internal class BaseTextHelper
+ {
+ internal static Element_AdaptiveText CreateBaseElement(IBaseText current)
+ {
+ return new Element_AdaptiveText()
+ {
+ Text = current.Text,
+ Lang = current.Language
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/BindableValues/BindableProgressBarValue.cs b/components/Notifications/src/Adaptive/BindableValues/BindableProgressBarValue.cs
new file mode 100644
index 000000000..1e84cb3f9
--- /dev/null
+++ b/components/Notifications/src/Adaptive/BindableValues/BindableProgressBarValue.cs
@@ -0,0 +1,88 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A binding value for doubles.
+ ///
+ public sealed class BindableProgressBarValue
+ {
+ ///
+ /// Gets raw value used for the implicit converter case, where dev provided a raw double. We store the raw value,
+ /// so that later on when generating the XML, we can provide this value rather than binding syntax.
+ ///
+ internal AdaptiveProgressBarValue RawValue { get; private set; }
+
+ internal bool RawIsIndeterminate { get; private set; }
+
+ ///
+ /// Gets or sets the name that maps to your binding data value.
+ ///
+ public string BindingName { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// A new binding for a double value, with the required binding value name. Do NOT include surrounding {} brackets.
+ ///
+ /// The name that maps to your binding data value.
+ public BindableProgressBarValue(string bindingName)
+ {
+ BindingName = bindingName;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Private constructor used by the implicit converter to assign the raw value.
+ ///
+ private BindableProgressBarValue()
+ {
+ }
+
+ internal string ToXmlString()
+ {
+ if (BindingName != null)
+ {
+ return "{" + BindingName + "}";
+ }
+
+ if (RawValue != null)
+ {
+ return RawValue.ToXmlString();
+ }
+
+ return null;
+ }
+
+ ///
+ /// Creates a that has a raw value assigned.
+ ///
+ /// The raw value
+ public static implicit operator BindableProgressBarValue(AdaptiveProgressBarValue v)
+ {
+ return new BindableProgressBarValue()
+ {
+ RawValue = v
+ };
+ }
+
+ ///
+ /// Returns the raw value of the .
+ ///
+ /// The to obtain the raw value from.
+ public static implicit operator AdaptiveProgressBarValue(BindableProgressBarValue b)
+ {
+ return b.RawValue;
+ }
+
+ ///
+ /// Creates an that has the raw double value.
+ ///
+ /// The raw value
+ public static implicit operator BindableProgressBarValue(double d)
+ {
+ return AdaptiveProgressBarValue.FromValue(d);
+ }
+ }
+}
diff --git a/components/Notifications/src/Adaptive/BindableValues/BindableString.cs b/components/Notifications/src/Adaptive/BindableValues/BindableString.cs
new file mode 100644
index 000000000..97849d70e
--- /dev/null
+++ b/components/Notifications/src/Adaptive/BindableValues/BindableString.cs
@@ -0,0 +1,68 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A binding value for strings.
+ ///
+ public sealed class BindableString
+ {
+ internal string RawValue { get; private set; }
+
+ ///
+ /// Gets or sets the name that maps to your binding data value.
+ ///
+ public string BindingName { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// A new binding for a string value, with the required binding name. Do NOT include surrounding {} brackets.
+ ///
+ /// The name that maps to your data binding value.
+ public BindableString(string bindingName)
+ {
+ BindingName = bindingName;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Private constructor used by the implicit converter to assign the raw value.
+ ///
+ private BindableString()
+ {
+ }
+
+ internal string ToXmlString()
+ {
+ if (BindingName != null)
+ {
+ return "{" + BindingName + "}";
+ }
+
+ return RawValue;
+ }
+
+ ///
+ /// Creates a that has a raw value assigned.
+ ///
+ /// The raw value
+ public static implicit operator BindableString(string d)
+ {
+ return new BindableString()
+ {
+ RawValue = d
+ };
+ }
+
+ ///
+ /// Returns the raw value of the .
+ ///
+ /// The to obtain the raw value from.
+ public static implicit operator string(BindableString b)
+ {
+ return b.RawValue;
+ }
+ }
+}
diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveGroup.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveGroup.cs
new file mode 100644
index 000000000..339ca0a03
--- /dev/null
+++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveGroup.cs
@@ -0,0 +1,34 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications.Adaptive.Elements
+{
+ internal sealed class Element_AdaptiveGroup : IElement_TileBindingChild, IElement_ToastBindingChild, IElementWithDescendants, IHaveXmlName, IHaveXmlChildren
+ {
+ public IList Children { get; private set; } = new List();
+
+ public IEnumerable Descendants()
+ {
+ foreach (Element_AdaptiveSubgroup subgroup in Children)
+ {
+ // Return the subgroup
+ yield return subgroup;
+
+ // And also return its descendants
+ foreach (object descendant in subgroup.Descendants())
+ {
+ yield return descendant;
+ }
+ }
+ }
+
+ ///
+ string IHaveXmlName.Name => "group";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Children;
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImage.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImage.cs
new file mode 100644
index 000000000..3ffe0b0f5
--- /dev/null
+++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImage.cs
@@ -0,0 +1,93 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications.Adaptive.Elements
+{
+ internal sealed class Element_AdaptiveImage : IElement_TileBindingChild, IElement_ToastBindingChild, IElement_AdaptiveSubgroupChild, IHaveXmlName, IHaveXmlNamedProperties
+ {
+ internal const AdaptiveImagePlacement DEFAULT_PLACEMENT = AdaptiveImagePlacement.Inline;
+ internal const AdaptiveImageCrop DEFAULT_CROP = AdaptiveImageCrop.Default;
+ internal const AdaptiveImageAlign DEFAULT_ALIGN = AdaptiveImageAlign.Default;
+
+ public int? Id { get; set; }
+
+ public string Src { get; set; }
+
+ public string Alt { get; set; }
+
+ public bool? AddImageQuery { get; set; }
+
+ public AdaptiveImagePlacement Placement { get; set; } = DEFAULT_PLACEMENT;
+
+ public AdaptiveImageAlign Align { get; set; } = DEFAULT_ALIGN;
+
+ public AdaptiveImageCrop Crop { get; set; } = DEFAULT_CROP;
+
+ public bool? RemoveMargin { get; set; }
+
+ private int? _overlay;
+
+ public int? Overlay
+ {
+ get
+ {
+ return _overlay;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ Element_TileBinding.CheckOverlayValue(value.Value);
+ }
+
+ _overlay = value;
+ }
+ }
+
+ public string SpriteSheetSrc { get; set; }
+
+ public uint? SpriteSheetHeight { get; set; }
+
+ public uint? SpriteSheetFps { get; set; }
+
+ public uint? SpriteSheetStartingFrame { get; set; }
+
+ ///
+ string IHaveXmlName.Name => "image";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("id", Id);
+ yield return new("src", Src);
+ yield return new("alt", Alt);
+ yield return new("addImageQuery", AddImageQuery);
+
+ if (Placement != DEFAULT_PLACEMENT)
+ {
+ yield return new("placement", Placement.ToPascalCaseString());
+ }
+
+ if (Align != DEFAULT_ALIGN)
+ {
+ yield return new("hint-align", Align.ToPascalCaseString());
+ }
+
+ if (Crop != DEFAULT_CROP)
+ {
+ yield return new("hint-crop", Crop.ToPascalCaseString());
+ }
+
+ yield return new("hint-removeMargin", RemoveMargin);
+ yield return new("hint-overlay", Overlay);
+ yield return new("spritesheet-src", SpriteSheetSrc);
+ yield return new("spritesheet-height", SpriteSheetHeight);
+ yield return new("spritesheet-fps", SpriteSheetFps);
+ yield return new("spritesheet-startingFrame", SpriteSheetStartingFrame);
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImageEnums.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImageEnums.cs
new file mode 100644
index 000000000..e817f880c
--- /dev/null
+++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveImageEnums.cs
@@ -0,0 +1,15 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements
+{
+ internal enum AdaptiveImagePlacement
+ {
+ Inline,
+ Background,
+ Peek,
+ Hero,
+ AppLogoOverride
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveProgressBar.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveProgressBar.cs
new file mode 100644
index 000000000..d26e96ee4
--- /dev/null
+++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveProgressBar.cs
@@ -0,0 +1,31 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications.Adaptive.Elements
+{
+ internal sealed class Element_AdaptiveProgressBar : IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties
+ {
+ public string Value { get; set; }
+
+ public string Title { get; set; }
+
+ public string ValueStringOverride { get; set; }
+
+ public string Status { get; set; }
+
+ ///
+ string IHaveXmlName.Name => "progress";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("value", Value);
+ yield return new("title", Title);
+ yield return new("valueStringOverride", ValueStringOverride);
+ yield return new("status", Status);
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveSubgroup.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveSubgroup.cs
new file mode 100644
index 000000000..be3a0953c
--- /dev/null
+++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveSubgroup.cs
@@ -0,0 +1,73 @@
+// 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;
+
+namespace CommunityToolkit.Notifications.Adaptive.Elements
+{
+ internal sealed class Element_AdaptiveSubgroup : IElementWithDescendants, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ internal const AdaptiveSubgroupTextStacking DEFAULT_TEXT_STACKING = AdaptiveSubgroupTextStacking.Default;
+
+ public AdaptiveSubgroupTextStacking TextStacking { get; set; } = DEFAULT_TEXT_STACKING;
+
+ private int? _weight;
+
+ public int? Weight
+ {
+ get
+ {
+ return _weight;
+ }
+
+ set
+ {
+ CheckWeight(value);
+
+ _weight = value;
+ }
+ }
+
+ internal static void CheckWeight(int? weight)
+ {
+ if (weight != null && weight.Value < 1)
+ {
+ throw new ArgumentOutOfRangeException("Weight must be between 1 and int.MaxValue, inclusive (or null)");
+ }
+ }
+
+ public IList Children { get; private set; } = new List();
+
+ public IEnumerable Descendants()
+ {
+ foreach (IElement_AdaptiveSubgroupChild child in Children)
+ {
+ // Return each child (we know there's no further descendants)
+ yield return child;
+ }
+ }
+
+ ///
+ string IHaveXmlName.Name => "subgroup";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Children;
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ if (TextStacking != DEFAULT_TEXT_STACKING)
+ {
+ yield return new("hint-textStacking", TextStacking.ToPascalCaseString());
+ }
+
+ yield return new("hint-weight", Weight);
+ }
+ }
+
+ internal interface IElement_AdaptiveSubgroupChild
+ {
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/Elements/Element_AdaptiveText.cs b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveText.cs
new file mode 100644
index 000000000..4b87fe9cb
--- /dev/null
+++ b/components/Notifications/src/Adaptive/Elements/Element_AdaptiveText.cs
@@ -0,0 +1,116 @@
+// 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;
+
+namespace CommunityToolkit.Notifications.Adaptive.Elements
+{
+ internal sealed class Element_AdaptiveText : IElement_TileBindingChild, IElement_AdaptiveSubgroupChild, IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlText
+ {
+ internal const AdaptiveTextStyle DEFAULT_STYLE = AdaptiveTextStyle.Default;
+ internal const AdaptiveTextAlign DEFAULT_ALIGN = AdaptiveTextAlign.Default;
+ internal const AdaptiveTextPlacement DEFAULT_PLACEMENT = AdaptiveTextPlacement.Inline;
+
+ public string Text { get; set; }
+
+ public int? Id { get; set; }
+
+ public string Lang { get; set; }
+
+ public AdaptiveTextAlign Align { get; set; } = DEFAULT_ALIGN;
+
+ private int? _maxLines;
+
+ public int? MaxLines
+ {
+ get
+ {
+ return _maxLines;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ CheckMaxLinesValue(value.Value);
+ }
+
+ _maxLines = value;
+ }
+ }
+
+ internal static void CheckMaxLinesValue(int value)
+ {
+ if (value < 1)
+ {
+ throw new ArgumentOutOfRangeException("MaxLines must be between 1 and int.MaxValue, inclusive.");
+ }
+ }
+
+ private int? _minLines;
+
+ public int? MinLines
+ {
+ get
+ {
+ return _minLines;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ CheckMinLinesValue(value.Value);
+ }
+
+ _minLines = value;
+ }
+ }
+
+ internal static void CheckMinLinesValue(int value)
+ {
+ if (value < 1)
+ {
+ throw new ArgumentOutOfRangeException("MinLines must be between 1 and int.MaxValue, inclusive.");
+ }
+ }
+
+ public AdaptiveTextStyle Style { get; set; } = DEFAULT_STYLE;
+
+ public bool? Wrap { get; set; }
+
+ public AdaptiveTextPlacement Placement { get; set; } = DEFAULT_PLACEMENT;
+
+ ///
+ string IHaveXmlName.Name => "text";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("id", Id);
+ yield return new("lang", Lang);
+
+ if (Align != DEFAULT_ALIGN)
+ {
+ yield return new("hint-align", Align.ToPascalCaseString());
+ }
+
+ yield return new("hint-maxLines", MaxLines);
+ yield return new("hint-minLines", MinLines);
+
+ if (Style != DEFAULT_STYLE)
+ {
+ yield return new("hint-style", Style.ToPascalCaseString());
+ }
+
+ yield return new("hint-wrap", Wrap);
+
+ if (Placement != DEFAULT_PLACEMENT)
+ {
+ yield return new("placement", Placement.ToPascalCaseString());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/IAdaptiveChild.cs b/components/Notifications/src/Adaptive/IAdaptiveChild.cs
new file mode 100644
index 000000000..1c345e518
--- /dev/null
+++ b/components/Notifications/src/Adaptive/IAdaptiveChild.cs
@@ -0,0 +1,14 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Elements that can be direct children of adaptive content, including ( , , and ).
+ ///
+ public interface IAdaptiveChild
+ {
+ // Blank interface simply for compile-enforcing the child types in the list.
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/IAdaptiveSubgroupChild.cs b/components/Notifications/src/Adaptive/IAdaptiveSubgroupChild.cs
new file mode 100644
index 000000000..9d257f032
--- /dev/null
+++ b/components/Notifications/src/Adaptive/IAdaptiveSubgroupChild.cs
@@ -0,0 +1,14 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Elements that can be direct children of an , including ( and ).
+ ///
+ public interface IAdaptiveSubgroupChild
+ {
+ // Blank interface simply for compile-enforcing the child types in the list.
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/IBaseImage.cs b/components/Notifications/src/Adaptive/IBaseImage.cs
new file mode 100644
index 000000000..c20127153
--- /dev/null
+++ b/components/Notifications/src/Adaptive/IBaseImage.cs
@@ -0,0 +1,27 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Contains the base properties that an image needs.
+ ///
+ public interface IBaseImage
+ {
+ ///
+ /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ ///
+ string Source { get; set; }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ bool? AddImageQuery { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Adaptive/IBaseText.cs b/components/Notifications/src/Adaptive/IBaseText.cs
new file mode 100644
index 000000000..38eb7bab4
--- /dev/null
+++ b/components/Notifications/src/Adaptive/IBaseText.cs
@@ -0,0 +1,22 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Defines the basic properties of a text element.
+ ///
+ public interface IBaseText
+ {
+ ///
+ /// Gets or sets the text to display.
+ ///
+ string Text { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ string Language { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Badges/BadgeGlyphContent.cs b/components/Notifications/src/Badges/BadgeGlyphContent.cs
new file mode 100644
index 000000000..5d02da2fd
--- /dev/null
+++ b/components/Notifications/src/Badges/BadgeGlyphContent.cs
@@ -0,0 +1,94 @@
+// 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 Windows.Data.Xml.Dom;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Notification content object to display a glyph on a Tile's badge.
+ ///
+ public sealed class BadgeGlyphContent : INotificationContent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Default constructor to create a glyph badge content object.
+ ///
+ public BadgeGlyphContent()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Constructor to create a glyph badge content object with a glyph.
+ ///
+ /// The glyph to be displayed on the badge.
+ public BadgeGlyphContent(BadgeGlyphValue glyph)
+ {
+ _glyph = glyph;
+ }
+
+ ///
+ /// Gets or sets the glyph to be displayed on the badge.
+ ///
+ public BadgeGlyphValue Glyph
+ {
+ get
+ {
+ return _glyph;
+ }
+
+ set
+ {
+ if (!Enum.IsDefined(typeof(BadgeGlyphValue), value))
+ {
+ throw new ArgumentOutOfRangeException(nameof(value));
+ }
+
+ _glyph = value;
+ }
+ }
+
+ ///
+ /// Retrieves the notification Xml content as a string.
+ ///
+ /// The notification Xml content as a string.
+ public string GetContent()
+ {
+ if (!Enum.IsDefined(typeof(BadgeGlyphValue), _glyph))
+ {
+ throw new NotificationContentValidationException("The badge glyph property was left unset.");
+ }
+
+ string glyphString = _glyph.ToString();
+
+ // lower case the first character of the enum value to match the Xml schema
+ glyphString = string.Format("{0}{1}", char.ToLowerInvariant(glyphString[0]), glyphString.Substring(1));
+ return string.Format(" ", glyphString);
+ }
+
+ ///
+ /// Retrieves the notification XML content as a string.
+ ///
+ /// The notification XML content as a string.
+ public override string ToString()
+ {
+ return GetContent();
+ }
+
+ ///
+ /// Retrieves the notification XML content as a WinRT Xml document.
+ ///
+ /// The notification XML content as a WinRT Xml document.
+ public XmlDocument GetXml()
+ {
+ XmlDocument xml = new XmlDocument();
+ xml.LoadXml(GetContent());
+ return xml;
+ }
+
+ private BadgeGlyphValue _glyph = (BadgeGlyphValue)(-1);
+ }
+}
diff --git a/components/Notifications/src/Badges/BadgeGlyphValue.cs b/components/Notifications/src/Badges/BadgeGlyphValue.cs
new file mode 100644
index 000000000..17bcb61b8
--- /dev/null
+++ b/components/Notifications/src/Badges/BadgeGlyphValue.cs
@@ -0,0 +1,78 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// The types of glyphs that can be placed on a badge.
+ ///
+ public enum BadgeGlyphValue
+ {
+ ///
+ /// No glyph. If there is a numeric badge, or a glyph currently on the badge,
+ /// it will be removed.
+ ///
+ None = 0,
+
+ ///
+ /// A glyph representing application activity.
+ ///
+ Activity,
+
+ ///
+ /// A glyph representing an alert.
+ ///
+ Alert,
+
+ ///
+ /// A glyph representing an alarm.
+ ///
+ Alarm,
+
+ ///
+ /// A glyph representing availability status.
+ ///
+ Available,
+
+ ///
+ /// A glyph representing away status
+ ///
+ Away,
+
+ ///
+ /// A glyph representing busy status.
+ ///
+ Busy,
+
+ ///
+ /// A glyph representing that a new message is available.
+ ///
+ NewMessage,
+
+ ///
+ /// A glyph representing that media is paused.
+ ///
+ Paused,
+
+ ///
+ /// A glyph representing that media is playing.
+ ///
+ Playing,
+
+ ///
+ /// A glyph representing unavailable status.
+ ///
+ Unavailable,
+
+ ///
+ /// A glyph representing an error.
+ ///
+ Error,
+
+ ///
+ /// A glyph representing attention status.
+ ///
+ Attention
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Badges/BadgeNumericContent.cs b/components/Notifications/src/Badges/BadgeNumericContent.cs
new file mode 100644
index 000000000..c3cda912b
--- /dev/null
+++ b/components/Notifications/src/Badges/BadgeNumericContent.cs
@@ -0,0 +1,76 @@
+// 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 Windows.Data.Xml.Dom;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Notification content object to display a number on a Tile's badge.
+ ///
+ public sealed class BadgeNumericContent : INotificationContent
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Default constructor to create a numeric badge content object.
+ ///
+ public BadgeNumericContent()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Constructor to create a numeric badge content object with a number.
+ ///
+ ///
+ /// The number that will appear on the badge. If the number is 0, the badge
+ /// will be removed.
+ ///
+ public BadgeNumericContent(uint number)
+ {
+ _number = number;
+ }
+
+ ///
+ /// Gets or sets the number that will appear on the badge. If the number is 0, the badge
+ /// will be removed.
+ ///
+ public uint Number
+ {
+ get { return _number; }
+ set { _number = value; }
+ }
+
+ ///
+ /// Retrieves the notification Xml content as a string.
+ ///
+ /// The notification Xml content as a string.
+ public string GetContent()
+ {
+ return string.Format(" ", _number);
+ }
+
+ ///
+ /// Retrieves the notification Xml content as a string.
+ ///
+ /// The notification Xml content as a string.
+ public override string ToString()
+ {
+ return GetContent();
+ }
+
+ ///
+ /// Retrieves the notification Xml content as a WinRT Xml document.
+ ///
+ /// The notification Xml content as a WinRT Xml document.
+ public XmlDocument GetXml()
+ {
+ XmlDocument xml = new XmlDocument();
+ xml.LoadXml(GetContent());
+ return xml;
+ }
+
+ private uint _number = 0;
+ }
+}
diff --git a/components/Notifications/src/Common/ArgumentValidator.cs b/components/Notifications/src/Common/ArgumentValidator.cs
new file mode 100644
index 000000000..c1f9c4d82
--- /dev/null
+++ b/components/Notifications/src/Common/ArgumentValidator.cs
@@ -0,0 +1,30 @@
+// 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 CommunityToolkit.Notifications
+{
+ internal static class ArgumentValidator
+ {
+ public static void SetProperty(ref T property, T value, string propertyName, ArgumentValidatorOptions options)
+ {
+ if (options.HasFlag(ArgumentValidatorOptions.NotNull))
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException(propertyName);
+ }
+ }
+
+ property = value;
+ }
+ }
+
+ [Flags]
+ internal enum ArgumentValidatorOptions
+ {
+ NotNull
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Common/BaseElement.cs b/components/Notifications/src/Common/BaseElement.cs
new file mode 100644
index 000000000..809eca5c1
--- /dev/null
+++ b/components/Notifications/src/Common/BaseElement.cs
@@ -0,0 +1,52 @@
+// 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.IO;
+using System.Text;
+
+using Windows.Data.Xml.Dom;
+
+namespace CommunityToolkit.Notifications
+{
+ internal abstract class BaseElement
+ {
+ ///
+ /// Retrieves the notification XML content as a string.
+ ///
+ /// The notification XML content as a string.
+ public string GetContent()
+ {
+ using (MemoryStream stream = new MemoryStream())
+ {
+ using (System.Xml.XmlWriter writer = System.Xml.XmlWriter.Create(stream, new System.Xml.XmlWriterSettings()
+ {
+ Encoding = Encoding.UTF8, // Use UTF-8 encoding to save space (it defaults to UTF-16 which is 2x the size)
+ Indent = false,
+ NewLineOnAttributes = false
+ }))
+ {
+ XmlWriterHelper.Write(writer, this);
+ }
+
+ stream.Position = 0;
+
+ using (StreamReader reader = new StreamReader(stream))
+ {
+ return reader.ReadToEnd();
+ }
+ }
+ }
+
+ ///
+ /// Retrieves the notification XML content as a WinRT XML document.
+ ///
+ /// The notification XML content as a WinRT XML document.
+ public XmlDocument GetXml()
+ {
+ XmlDocument xml = new XmlDocument();
+ xml.LoadXml(GetContent());
+ return xml;
+ }
+ }
+}
diff --git a/components/Notifications/src/Common/EnumFormatter.cs b/components/Notifications/src/Common/EnumFormatter.cs
new file mode 100644
index 000000000..117a98a4f
--- /dev/null
+++ b/components/Notifications/src/Common/EnumFormatter.cs
@@ -0,0 +1,57 @@
+// 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;
+
+#nullable enable
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// A helper class that can be used to format values.
+ ///
+ internal static class EnumFormatter
+ {
+ ///
+ /// Returns a representation of an enum value with pascal casing.
+ ///
+ /// The type to format.
+ /// The value to format.
+ /// The pascal case representation of .
+ public static string? ToPascalCaseString(this T? value)
+ where T : unmanaged, Enum
+ {
+ if (value is null)
+ {
+ return null;
+ }
+
+ return ToPascalCaseString(value.Value);
+ }
+
+ ///
+ /// Returns a representation of an enum value with pascal casing.
+ ///
+ /// The type to format.
+ /// The value to format.
+ /// The pascal case representation of .
+ public static string? ToPascalCaseString(this T value)
+ where T : unmanaged, Enum
+ {
+ string? text = value.ToString();
+
+ if (text is null or { Length: 0 })
+ {
+ return text;
+ }
+
+ if (text is { Length: 1 })
+ {
+ return text.ToLowerInvariant();
+ }
+
+ return $"{char.ToLowerInvariant(text[0])}{text.Substring(1)}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Common/INotificationContent.cs b/components/Notifications/src/Common/INotificationContent.cs
new file mode 100644
index 000000000..d84afe4a3
--- /dev/null
+++ b/components/Notifications/src/Common/INotificationContent.cs
@@ -0,0 +1,26 @@
+// 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 Windows.Data.Xml.Dom;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Base notification content interface to retrieve notification Xml as a string.
+ ///
+ public interface INotificationContent
+ {
+ ///
+ /// Retrieves the notification Xml content as a string.
+ ///
+ /// The notification Xml content as a string.
+ string GetContent();
+
+ ///
+ /// Retrieves the notification Xml content as a WinRT Xml document.
+ ///
+ /// The notification Xml content as a WinRT Xml document.
+ XmlDocument GetXml();
+ }
+}
diff --git a/components/Notifications/src/Common/LimitedList{T}.cs b/components/Notifications/src/Common/LimitedList{T}.cs
new file mode 100644
index 000000000..5ff4ac43b
--- /dev/null
+++ b/components/Notifications/src/Common/LimitedList{T}.cs
@@ -0,0 +1,113 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class LimitedList : IList
+ {
+ private List _list;
+
+ public int Limit { get; private set; }
+
+ public LimitedList(int limit)
+ {
+ _list = new List(limit);
+
+ Limit = limit;
+ }
+
+ public T this[int index]
+ {
+ get
+ {
+ return _list[index];
+ }
+
+ set
+ {
+ _list[index] = value;
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ return _list.Count;
+ }
+ }
+
+ public bool IsReadOnly
+ {
+ get
+ {
+ return false;
+ }
+ }
+
+ public void Add(T item)
+ {
+ if (_list.Count >= Limit)
+ {
+ throw new Exception("This list is limited to " + Limit + " items. You cannot add more items.");
+ }
+
+ _list.Add(item);
+ }
+
+ public void Clear()
+ {
+ _list.Clear();
+ }
+
+ public bool Contains(T item)
+ {
+ return _list.Contains(item);
+ }
+
+ public void CopyTo(T[] array, int arrayIndex)
+ {
+ _list.CopyTo(array, arrayIndex);
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ public int IndexOf(T item)
+ {
+ return _list.IndexOf(item);
+ }
+
+ public void Insert(int index, T item)
+ {
+ _list.Insert(index, item);
+ }
+
+ public bool Remove(T item)
+ {
+ return _list.Remove(item);
+ }
+
+ public void RemoveAt(int index)
+ {
+ _list.RemoveAt(index);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+
+ internal interface IElementWithDescendants
+ {
+ IEnumerable Descendants();
+ }
+}
diff --git a/components/Notifications/src/Common/NotificationContentValidationException.cs b/components/Notifications/src/Common/NotificationContentValidationException.cs
new file mode 100644
index 000000000..2963aa444
--- /dev/null
+++ b/components/Notifications/src/Common/NotificationContentValidationException.cs
@@ -0,0 +1,19 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Exception returned when invalid notification content is provided.
+ ///
+ internal sealed class NotificationContentValidationException : Exception
+ {
+ public NotificationContentValidationException(string message)
+ : base(message)
+ {
+ }
+ }
+}
diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlAdditionalProperties.cs b/components/Notifications/src/Common/Serialization/IHaveXmlAdditionalProperties.cs
new file mode 100644
index 000000000..757d1f5d7
--- /dev/null
+++ b/components/Notifications/src/Common/Serialization/IHaveXmlAdditionalProperties.cs
@@ -0,0 +1,21 @@
+// 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.Collections.Generic;
+
+#nullable enable
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// An interface for a notification XML element with additional properties.
+ ///
+ internal interface IHaveXmlAdditionalProperties
+ {
+ ///
+ /// Gets the mapping of additional properties.
+ ///
+ IReadOnlyDictionary AdditionalProperties { get; }
+ }
+}
diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlChildren.cs b/components/Notifications/src/Common/Serialization/IHaveXmlChildren.cs
new file mode 100644
index 000000000..12723a323
--- /dev/null
+++ b/components/Notifications/src/Common/Serialization/IHaveXmlChildren.cs
@@ -0,0 +1,21 @@
+// 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.Collections.Generic;
+
+#nullable enable
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// An interface for a notification XML element with additional children.
+ ///
+ internal interface IHaveXmlChildren
+ {
+ ///
+ /// Gets the children of the current element.
+ ///
+ IEnumerable Children { get; }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlName.cs b/components/Notifications/src/Common/Serialization/IHaveXmlName.cs
new file mode 100644
index 000000000..f9e96c99d
--- /dev/null
+++ b/components/Notifications/src/Common/Serialization/IHaveXmlName.cs
@@ -0,0 +1,17 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// An interface for a notification XML element with a name.
+ ///
+ internal interface IHaveXmlName
+ {
+ ///
+ /// Gets the name of the current element.
+ ///
+ string Name { get; }
+ }
+}
diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlNamedProperties.cs b/components/Notifications/src/Common/Serialization/IHaveXmlNamedProperties.cs
new file mode 100644
index 000000000..29c7e17b5
--- /dev/null
+++ b/components/Notifications/src/Common/Serialization/IHaveXmlNamedProperties.cs
@@ -0,0 +1,23 @@
+// 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.Collections.Generic;
+
+#nullable enable
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// An interface for a notification XML element with named properties.
+ ///
+ internal interface IHaveXmlNamedProperties
+ {
+ ///
+ /// Enumerates the available named properties for the element.
+ ///
+ /// A sequence of named properties for the element.
+ /// The returned values must be valid XML values when is called on them.
+ IEnumerable> EnumerateNamedProperties();
+ }
+}
diff --git a/components/Notifications/src/Common/Serialization/IHaveXmlText.cs b/components/Notifications/src/Common/Serialization/IHaveXmlText.cs
new file mode 100644
index 000000000..358afa31b
--- /dev/null
+++ b/components/Notifications/src/Common/Serialization/IHaveXmlText.cs
@@ -0,0 +1,17 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// An interface for a notification XML element with an explicit XML text content.
+ ///
+ internal interface IHaveXmlText
+ {
+ ///
+ /// Gets the text content of the current element.
+ ///
+ string Text { get; }
+ }
+}
diff --git a/components/Notifications/src/Common/XmlWriterHelper.cs b/components/Notifications/src/Common/XmlWriterHelper.cs
new file mode 100644
index 000000000..6111d2bda
--- /dev/null
+++ b/components/Notifications/src/Common/XmlWriterHelper.cs
@@ -0,0 +1,87 @@
+// 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 System.Xml;
+
+namespace CommunityToolkit.Notifications
+{
+ internal static class XmlWriterHelper
+ {
+ public static void Write(XmlWriter writer, object element)
+ {
+ // If it isn't an XML element, don't write anything
+ if (element is not IHaveXmlName xmlElement)
+ {
+ return;
+ }
+
+ writer.WriteStartElement(xmlElement.Name);
+
+ // Write all named properties
+ foreach (var property in (element as IHaveXmlNamedProperties)?.EnumerateNamedProperties() ?? Enumerable.Empty>())
+ {
+ if (property.Value is not null)
+ {
+ writer.WriteAttributeString(property.Key, PropertyValueToString(property.Value));
+ }
+ }
+
+ // Write all additional properties
+ foreach (var property in (element as IHaveXmlAdditionalProperties)?.AdditionalProperties ?? Enumerable.Empty>())
+ {
+ writer.WriteAttributeString(property.Key, property.Value);
+ }
+
+ // Write the inner text, if any
+ if ((element as IHaveXmlText)?.Text is string { Length: > 0 } text)
+ {
+ writer.WriteString(text);
+ }
+
+ // Write all children, if any
+ foreach (var child in (element as IHaveXmlChildren)?.Children ?? Enumerable.Empty())
+ {
+ Write(writer, child);
+ }
+
+ writer.WriteEndElement();
+ }
+
+ private static string PropertyValueToString(object propertyValue)
+ {
+ return propertyValue switch
+ {
+ true => "true",
+ false => "false",
+ DateTimeOffset dateTime => XmlConvert.ToString(dateTime), // ISO 8601 format
+ { } value => value.ToString(),
+ _ => null
+ };
+ }
+
+ ///
+ /// Gets the provided binding value, if it exists. Otherwise, falls back to the absolute value.
+ ///
+ /// The type of the enum of the class properties.
+ /// The collection of data-bound values.
+ /// The property to obtain.
+ /// The absolute value, if any.
+ /// The provided binding value, if it exists. Otherwise, falls back to the absolute value.
+ internal static string GetBindingOrAbsoluteXmlValue(IDictionary bindings, T bindableProperty, string absoluteValue)
+ {
+ // If a binding is provided, use the binding value
+ string bindingValue;
+ if (bindings.TryGetValue(bindableProperty, out bindingValue))
+ {
+ return "{" + bindingValue + "}";
+ }
+
+ // Otherwise fallback to the absolute value
+ return absoluteValue;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/CommunityToolkit.Notifications.csproj b/components/Notifications/src/CommunityToolkit.Notifications.csproj
new file mode 100644
index 000000000..4112bc6c6
--- /dev/null
+++ b/components/Notifications/src/CommunityToolkit.Notifications.csproj
@@ -0,0 +1,21 @@
+
+
+
+
+ Notifications
+
+ The official way to send toast notifications on Windows 10 via code rather than XML, with the help of IntelliSense. Supports all C# app types, including WPF, UWP, WinForms, and Console, even without packaging your app as MSIX. Also supports C++ UWP apps.
+
+ Additionally, generate notification payloads from your ASP.NET web server to send as push notifications, or generate notification payloads from class libraries.
+
+ For UWP/MSIX apps, you can also generate tile and badge notifications.
+
+ CommunityToolkit.Notifications
+ $(PackageIdPrefix).$(ToolkitComponentName)
+ disable
+ false
+
+
+
+
+
diff --git a/components/Notifications/src/Dependencies.props b/components/Notifications/src/Dependencies.props
new file mode 100644
index 000000000..e622e1df4
--- /dev/null
+++ b/components/Notifications/src/Dependencies.props
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/components/Notifications/src/MultiTarget.props b/components/Notifications/src/MultiTarget.props
new file mode 100644
index 000000000..e63031231
--- /dev/null
+++ b/components/Notifications/src/MultiTarget.props
@@ -0,0 +1,9 @@
+
+
+
+ uwp;
+
+
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/Builder/TileContentBuilder.SpecialTiles.cs b/components/Notifications/src/Tiles/Builder/TileContentBuilder.SpecialTiles.cs
new file mode 100644
index 000000000..a706c5b56
--- /dev/null
+++ b/components/Notifications/src/Tiles/Builder/TileContentBuilder.SpecialTiles.cs
@@ -0,0 +1,161 @@
+// 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 System.Text;
+
+namespace CommunityToolkit.Notifications
+{
+#pragma warning disable SA1008
+#pragma warning disable SA1009
+ ///
+ /// Builder class used to create
+ ///
+ public partial class TileContentBuilder
+ {
+ ///
+ /// Helper method for creating a tile notification content for using Contact tile template.
+ ///
+ /// Source for the contact picture
+ /// Name of the contact
+ /// A description of the contact image, for users of assistive technologies.
+ /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification.
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentContact CreateContactTileContent(Uri contactImageUri, string contactName, string contactImageAltText = default(string), bool? contactImageAddImageQuery = default(bool?), string textLanguage = default(string))
+ {
+ var contactTileContent = new TileBindingContentContact();
+ contactTileContent.Image = CreateTileBasicImage(contactImageUri, contactImageAltText, contactImageAddImageQuery);
+
+ contactTileContent.Text = new TileBasicText();
+ contactTileContent.Text.Text = contactName;
+
+ if (textLanguage != default(string))
+ {
+ contactTileContent.Text.Lang = textLanguage;
+ }
+
+ return contactTileContent;
+ }
+
+ ///
+ /// Helper method for creating a tile notification content for using Iconic tile template.
+ ///
+ /// Source of the icon image.
+ /// A description of the icon image, for users of assistive technologies.
+ /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentIconic CreateIconicTileContent(Uri iconImageUri, string iconImageAltText = default(string), bool? iconImageAddImageQuery = default(bool?))
+ {
+ var iconicTileContent = new TileBindingContentIconic();
+ iconicTileContent.Icon = CreateTileBasicImage(iconImageUri, iconImageAltText, iconImageAddImageQuery);
+
+ return iconicTileContent;
+ }
+
+ ///
+ /// Helper method for creating a tile notification content for using People tile template.
+ ///
+ /// Sources of pictures that will be used on the people tile.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentPeople CreatePeopleTileContent(params Uri[] peoplePictureSources)
+ {
+ IEnumerable images = peoplePictureSources.Select(u => CreateTileBasicImage(u, default(string), default(bool?)));
+
+ return CreatePeopleTileContent(images);
+ }
+
+ ///
+ /// Helper method for creating a tile notification content for using People tile template.
+ ///
+ /// Sources of pictures with description and image query indicator that will be used on the people tile.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentPeople CreatePeopleTileContent(params (Uri source, string imageAltText, bool? addImageQuery)[] peoplePictures)
+ {
+ IEnumerable images = peoplePictures.Select(t => CreateTileBasicImage(t.source, t.imageAltText, t.addImageQuery));
+
+ return CreatePeopleTileContent(images);
+ }
+
+ ///
+ /// Helper method for creating a tile notification content for using People tile template.
+ ///
+ /// Pictures that will be used on the people tile.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentPeople CreatePeopleTileContent(IEnumerable peoplePictures)
+ {
+ var peopleTileContent = new TileBindingContentPeople();
+
+ foreach (var image in peoplePictures)
+ {
+ peopleTileContent.Images.Add(image);
+ }
+
+ return peopleTileContent;
+ }
+
+ ///
+ /// Helper method for creating a tile notification content for using Photos tile template.
+ ///
+ /// Sources of pictures that will be used on the photos tile.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentPhotos CreatePhotosTileContent(params Uri[] photoSources)
+ {
+ IEnumerable images = photoSources.Select(u => CreateTileBasicImage(u, default(string), default(bool?)));
+
+ return CreatePhotosTileContent(images);
+ }
+
+ ///
+ /// Helper method for creating a tile notification content for using Photos tile template.
+ ///
+ /// Sources of pictures with description and addImageQuery indicator that will be used for the photos tile.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentPhotos CreatePhotosTileContent(params (Uri source, string imageAltText, bool? addImageQuery)[] photos)
+ {
+ IEnumerable images = photos.Select(t => CreateTileBasicImage(t.source, t.imageAltText, t.addImageQuery));
+
+ return CreatePhotosTileContent(images);
+ }
+
+ ///
+ /// Helper method for creating a tile notification content for using Photos tile template.
+ ///
+ /// Pictures that will be used for the photos tile.
+ /// An instance of represent a payload of a tile notification.
+ public static TileBindingContentPhotos CreatePhotosTileContent(IEnumerable photos)
+ {
+ var photoTileContent = new TileBindingContentPhotos();
+
+ foreach (var image in photos)
+ {
+ photoTileContent.Images.Add(image);
+ }
+
+ return photoTileContent;
+ }
+
+ private static TileBasicImage CreateTileBasicImage(Uri imageUri, string imageAltText, bool? addImageQuery)
+ {
+ var tileImage = new TileBasicImage();
+ tileImage.Source = imageUri.OriginalString;
+
+ if (imageAltText != default(string))
+ {
+ tileImage.AlternateText = imageAltText;
+ }
+
+ if (addImageQuery != default(bool?))
+ {
+ tileImage.AddImageQuery = addImageQuery;
+ }
+
+ return tileImage;
+ }
+ }
+#pragma warning restore SA1008
+#pragma warning restore SA1009
+}
diff --git a/components/Notifications/src/Tiles/Builder/TileContentBuilder.cs b/components/Notifications/src/Tiles/Builder/TileContentBuilder.cs
new file mode 100644
index 000000000..e8e2cb71c
--- /dev/null
+++ b/components/Notifications/src/Tiles/Builder/TileContentBuilder.cs
@@ -0,0 +1,527 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Builder class used to create
+ ///
+ public partial class TileContentBuilder
+ {
+ ///
+ /// Flag used to create all tile size (Small , Medium, Large and Wide)
+ ///
+ public const TileSize AllSize = TileSize.Small | TileSize.Medium | TileSize.Large | TileSize.Wide;
+
+ ///
+ /// Gets internal instance of . This is equivalent to the call to .
+ ///
+ public TileContent Content
+ {
+ get; private set;
+ }
+
+ private TileVisual Visual
+ {
+ get
+ {
+ if (Content.Visual == null)
+ {
+ Content.Visual = new TileVisual();
+ }
+
+ return Content.Visual;
+ }
+ }
+
+ private TileBinding SmallTile
+ {
+ get
+ {
+ return Visual.TileSmall;
+ }
+
+ set
+ {
+ Visual.TileSmall = value;
+ }
+ }
+
+ private TileBinding MediumTile
+ {
+ get
+ {
+ return Visual.TileMedium;
+ }
+
+ set
+ {
+ Visual.TileMedium = value;
+ }
+ }
+
+ private TileBinding WideTile
+ {
+ get
+ {
+ return Visual.TileWide;
+ }
+
+ set
+ {
+ Visual.TileWide = value;
+ }
+ }
+
+ private TileBinding LargeTile
+ {
+ get
+ {
+ return Visual.TileLarge;
+ }
+
+ set
+ {
+ Visual.TileLarge = value;
+ }
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public TileContentBuilder()
+ {
+ Content = new TileContent();
+ }
+
+ ///
+ /// Add a tile layout size that the notification will be displayed on.
+ ///
+ /// The size of tile that the notification will be displayed on.
+ /// Specialized tile content. Use for special tile template. Default to NULL.
+ /// The current instance of
+ public TileContentBuilder AddTile(TileSize size, ITileBindingContent tileContent = null)
+ {
+ if (size.HasFlag(TileSize.Small))
+ {
+ SmallTile = new TileBinding();
+ SmallTile.Content = tileContent ?? new TileBindingContentAdaptive();
+ }
+
+ if (size.HasFlag(TileSize.Medium))
+ {
+ MediumTile = new TileBinding();
+ MediumTile.Content = tileContent ?? new TileBindingContentAdaptive();
+ }
+
+ if (size.HasFlag(TileSize.Wide))
+ {
+ WideTile = new TileBinding();
+ WideTile.Content = tileContent ?? new TileBindingContentAdaptive();
+ }
+
+ if (size.HasFlag(TileSize.Large))
+ {
+ LargeTile = new TileBinding();
+ LargeTile.Content = tileContent ?? new TileBindingContentAdaptive();
+ }
+
+ return this;
+ }
+
+ ///
+ /// Set how the tile notification should display the application branding.
+ ///
+ /// How branding should appear on the tile
+ /// The tile size that the parameter should be applied to. Default to all currently supported tile size.
+ /// The current instance of
+ public TileContentBuilder SetBranding(TileBranding branding, TileSize size = AllSize)
+ {
+ if (size == AllSize)
+ {
+ // Set on visual.
+ Visual.Branding = branding;
+ }
+ else
+ {
+ if (size.HasFlag(TileSize.Small) && SmallTile != null)
+ {
+ SmallTile.Branding = branding;
+ }
+
+ if (size.HasFlag(TileSize.Medium) && MediumTile != null)
+ {
+ MediumTile.Branding = branding;
+ }
+
+ if (size.HasFlag(TileSize.Wide) && WideTile != null)
+ {
+ WideTile.Branding = branding;
+ }
+
+ if (size.HasFlag(TileSize.Large) && LargeTile != null)
+ {
+ LargeTile.Branding = branding;
+ }
+ }
+
+ return this;
+ }
+
+ ///
+ /// Set the name that will be used to override the application's name on the tile notification.
+ ///
+ /// Custom name to display on the tile in place of the application's name
+ /// The tile size that parameter should be applied to. Default to all currently supported tile size.
+ /// The current instance of
+ public TileContentBuilder SetDisplayName(string displayName, TileSize size = AllSize)
+ {
+ if (size == AllSize)
+ {
+ // Set on visual.
+ Visual.DisplayName = displayName;
+ }
+ else
+ {
+ if (size.HasFlag(TileSize.Small) && SmallTile != null)
+ {
+ SmallTile.DisplayName = displayName;
+ }
+
+ if (size.HasFlag(TileSize.Medium) && MediumTile != null)
+ {
+ MediumTile.DisplayName = displayName;
+ }
+
+ if (size.HasFlag(TileSize.Wide) && WideTile != null)
+ {
+ WideTile.DisplayName = displayName;
+ }
+
+ if (size.HasFlag(TileSize.Large) && LargeTile != null)
+ {
+ LargeTile.DisplayName = displayName;
+ }
+ }
+
+ return this;
+ }
+
+ ///
+ /// Set the optional background image that stays behind the tile notification.
+ ///
+ /// Source of the background image
+ /// The tile size that the background image should be applied to. Default to all currently supported tile size.
+ /// Description of the background image, for user of assistance technology
+ ///
+ /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification.
+ /// Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string.
+ /// This query string specifies scale, contrast setting, and language.
+ ///
+ /// The opacity of the black overlay on the background image.
+ /// Desired cropping of the image.
+ /// The current instance of
+ public TileContentBuilder SetBackgroundImage(Uri imageUri, TileSize size = AllSize, string alternateText = default(string), bool? addImageQuery = default(bool?), int? hintOverlay = default(int?), TileBackgroundImageCrop hintCrop = TileBackgroundImageCrop.Default)
+ {
+ TileBackgroundImage backgroundImage = new TileBackgroundImage();
+ backgroundImage.Source = imageUri.OriginalString;
+ backgroundImage.HintCrop = hintCrop;
+
+ if (alternateText != default(string))
+ {
+ backgroundImage.AlternateText = alternateText;
+ }
+
+ if (addImageQuery != default(bool?))
+ {
+ backgroundImage.AddImageQuery = addImageQuery;
+ }
+
+ if (hintOverlay != default(int?))
+ {
+ backgroundImage.HintOverlay = hintOverlay;
+ }
+
+ return SetBackgroundImage(backgroundImage, size);
+ }
+
+ ///
+ /// Set the optional background image that stays behind the tile notification.
+ ///
+ /// An instance of as the background image for the tile.
+ /// The tile size that the background image should be applied to. Default to all currently supported tile size.
+ /// The current instance of
+ public TileContentBuilder SetBackgroundImage(TileBackgroundImage backgroundImage, TileSize size = AllSize)
+ {
+ // Set to any available tile at the moment of calling.
+ if (size.HasFlag(TileSize.Small) && SmallTile != null)
+ {
+ GetAdaptiveTileContent(SmallTile).BackgroundImage = backgroundImage;
+ }
+
+ if (size.HasFlag(TileSize.Medium) && MediumTile != null)
+ {
+ GetAdaptiveTileContent(MediumTile).BackgroundImage = backgroundImage;
+ }
+
+ if (size.HasFlag(TileSize.Wide) && WideTile != null)
+ {
+ GetAdaptiveTileContent(WideTile).BackgroundImage = backgroundImage;
+ }
+
+ if (size.HasFlag(TileSize.Large) && LargeTile != null)
+ {
+ GetAdaptiveTileContent(LargeTile).BackgroundImage = backgroundImage;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Set the Tile's Peek Image that animate from the top of the tile notification.
+ ///
+ /// Source of the peek image
+ /// The tile size that the peek image should be applied to. Default to all currently supported tile size.
+ /// Description of the peek image, for user of assistance technology
+ ///
+ /// Indicating whether Windows should append a query string to the image URI supplied in the Tile notification.
+ /// Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string.
+ /// This query string specifies scale, contrast setting, and language.
+ ///
+ /// The opacity of the black overlay on the peek image.
+ /// Desired cropping of the image.
+ /// The current instance of
+ public TileContentBuilder SetPeekImage(Uri imageUri, TileSize size = AllSize, string alternateText = default(string), bool? addImageQuery = default(bool?), int? hintOverlay = default(int?), TilePeekImageCrop hintCrop = TilePeekImageCrop.Default)
+ {
+ TilePeekImage peekImage = new TilePeekImage();
+ peekImage.Source = imageUri.OriginalString;
+ peekImage.HintCrop = hintCrop;
+
+ if (alternateText != default(string))
+ {
+ peekImage.AlternateText = alternateText;
+ }
+
+ if (addImageQuery != default(bool?))
+ {
+ peekImage.AddImageQuery = addImageQuery;
+ }
+
+ if (hintOverlay != default(int?))
+ {
+ peekImage.HintOverlay = hintOverlay;
+ }
+
+ return SetPeekImage(peekImage, size);
+ }
+
+ ///
+ /// Set the Tile's Peek Image that animate from the top of the tile notification.
+ ///
+ /// An instance of for the Tile's peek image
+ /// The tile size that the peek image should be applied to. Default to all currently supported tile size.
+ /// The current instance of
+ public TileContentBuilder SetPeekImage(TilePeekImage peekImage, TileSize size = AllSize)
+ {
+ // Set to any available tile at the moment of calling.
+ if (size.HasFlag(TileSize.Small) && SmallTile != null)
+ {
+ GetAdaptiveTileContent(SmallTile).PeekImage = peekImage;
+ }
+
+ if (size.HasFlag(TileSize.Medium) && MediumTile != null)
+ {
+ GetAdaptiveTileContent(MediumTile).PeekImage = peekImage;
+ }
+
+ if (size.HasFlag(TileSize.Wide) && WideTile != null)
+ {
+ GetAdaptiveTileContent(WideTile).PeekImage = peekImage;
+ }
+
+ if (size.HasFlag(TileSize.Large) && LargeTile != null)
+ {
+ GetAdaptiveTileContent(LargeTile).PeekImage = peekImage;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Set the text stacking (vertical alignment) of the entire binding element.
+ ///
+ /// Text Stacking Option
+ /// The tile size that the peek image should be applied to. Default to all currently supported tile size.
+ /// The current instance of
+ public TileContentBuilder SetTextStacking(TileTextStacking textStacking, TileSize size = AllSize)
+ {
+ // Set to any available tile at the moment of calling.
+ if (size.HasFlag(TileSize.Small) && SmallTile != null)
+ {
+ GetAdaptiveTileContent(SmallTile).TextStacking = textStacking;
+ }
+
+ if (size.HasFlag(TileSize.Medium) && MediumTile != null)
+ {
+ GetAdaptiveTileContent(MediumTile).TextStacking = textStacking;
+ }
+
+ if (size.HasFlag(TileSize.Wide) && WideTile != null)
+ {
+ GetAdaptiveTileContent(WideTile).TextStacking = textStacking;
+ }
+
+ if (size.HasFlag(TileSize.Large) && LargeTile != null)
+ {
+ GetAdaptiveTileContent(LargeTile).TextStacking = textStacking;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Set the tile's activation arguments for tile notification.
+ ///
+ /// App-Defined custom arguments that will be passed in when the user click on the tile when this tile notification is being displayed.
+ /// The tile size that the custom argument should be applied to. Default to all currently supported tile size.
+ /// The current instance of
+ public TileContentBuilder SetActivationArgument(string args, TileSize size = AllSize)
+ {
+ if (size == AllSize)
+ {
+ Visual.Arguments = args;
+ }
+ else
+ {
+ if (size.HasFlag(TileSize.Small) && SmallTile != null)
+ {
+ SmallTile.Arguments = args;
+ }
+
+ if (size.HasFlag(TileSize.Medium) && MediumTile != null)
+ {
+ MediumTile.Arguments = args;
+ }
+
+ if (size.HasFlag(TileSize.Wide) && WideTile != null)
+ {
+ WideTile.Arguments = args;
+ }
+
+ if (size.HasFlag(TileSize.Large) && LargeTile != null)
+ {
+ LargeTile.Arguments = args;
+ }
+ }
+
+ return this;
+ }
+
+ ///
+ /// Add a custom text that will appear on the tile notification.
+ ///
+ /// Custom text to display on the tile.
+ /// The tile size that the custom text would be added to. Default to all currently supported tile size.
+ /// Style that controls the text's font size, weight, and opacity.
+ /// Indicating whether text wrapping is enabled. For Tiles, this is false by default.
+ /// The maximum number of lines the text element is allowed to display. For Tiles, this is infinity by default
+ /// The minimum number of lines the text element must display.
+ /// The horizontal alignment of the text
+ ///
+ /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual.
+ ///
+ /// The current instance of
+ public TileContentBuilder AddText(string text, TileSize size = AllSize, AdaptiveTextStyle? hintStyle = null, bool? hintWrap = default(bool?), int? hintMaxLines = default(int?), int? hintMinLines = default(int?), AdaptiveTextAlign? hintAlign = null, string language = default(string))
+ {
+ // Create the adaptive text.
+ AdaptiveText adaptive = new AdaptiveText()
+ {
+ Text = text
+ };
+
+ if (hintStyle != null)
+ {
+ adaptive.HintStyle = hintStyle.Value;
+ }
+
+ if (hintAlign != null)
+ {
+ adaptive.HintAlign = hintAlign.Value;
+ }
+
+ if (hintWrap != default(bool?))
+ {
+ adaptive.HintWrap = hintWrap;
+ }
+
+ if (hintMaxLines != default(int?))
+ {
+ adaptive.HintMaxLines = hintMaxLines;
+ }
+
+ if (hintMinLines != default(int?) && hintMinLines > 0)
+ {
+ adaptive.HintMinLines = hintMinLines;
+ }
+
+ if (language != default(string))
+ {
+ adaptive.Language = language;
+ }
+
+ // Add to the tile content
+ return AddAdaptiveTileVisualChild(adaptive, size);
+ }
+
+ ///
+ /// Add an adaptive child to the tile notification.
+ ///
+ /// An adaptive child to add
+ /// Tile size that the adaptive child should be added to. Default to all currently supported tile size.
+ /// The current instance of
+ ///
+ /// This can be used to add Group and Subgroup to the tile.
+ ///
+ public TileContentBuilder AddAdaptiveTileVisualChild(ITileBindingContentAdaptiveChild child, TileSize size = AllSize)
+ {
+ if (size.HasFlag(TileSize.Small) && SmallTile != null && GetAdaptiveTileContent(SmallTile) != null)
+ {
+ GetAdaptiveTileContent(SmallTile).Children.Add(child);
+ }
+
+ if (size.HasFlag(TileSize.Medium) && MediumTile != null && GetAdaptiveTileContent(MediumTile) != null)
+ {
+ GetAdaptiveTileContent(MediumTile).Children.Add(child);
+ }
+
+ if (size.HasFlag(TileSize.Wide) && WideTile != null && GetAdaptiveTileContent(WideTile) != null)
+ {
+ GetAdaptiveTileContent(WideTile).Children.Add(child);
+ }
+
+ if (size.HasFlag(TileSize.Large) && LargeTile != null && GetAdaptiveTileContent(LargeTile) != null)
+ {
+ GetAdaptiveTileContent(LargeTile).Children.Add(child);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Get the instance of that has been built by the builder with specified configuration so far.
+ ///
+ /// An instance of that can be used to create tile notification.
+ public TileContent GetTileContent()
+ {
+ return Content;
+ }
+
+ private TileBindingContentAdaptive GetAdaptiveTileContent(TileBinding binding)
+ {
+ return binding.Content as TileBindingContentAdaptive;
+ }
+ }
+}
diff --git a/components/Notifications/src/Tiles/Elements/Element_Tile.cs b/components/Notifications/src/Tiles/Elements/Element_Tile.cs
new file mode 100644
index 000000000..de1db439c
--- /dev/null
+++ b/components/Notifications/src/Tiles/Elements/Element_Tile.cs
@@ -0,0 +1,19 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_Tile : BaseElement, IHaveXmlName, IHaveXmlChildren
+ {
+ public Element_TileVisual Visual { get; set; }
+
+ ///
+ string IHaveXmlName.Name => "tile";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => new[] { Visual };
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/Elements/Element_TileBinding.cs b/components/Notifications/src/Tiles/Elements/Element_TileBinding.cs
new file mode 100644
index 000000000..1125354e7
--- /dev/null
+++ b/components/Notifications/src/Tiles/Elements/Element_TileBinding.cs
@@ -0,0 +1,146 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_TileBinding : IElementWithDescendants, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ internal const TileBranding DEFAULT_BRANDING = TileBranding.Auto;
+ internal const TileTextStacking DEFAULT_TEXT_STACKING = TileTextStacking.Top;
+ internal const int DEFAULT_OVERLAY = 20;
+
+ public Element_TileBinding(TileTemplateNameV3 template)
+ {
+ Template = template;
+ }
+
+ public TileTemplateNameV3 Template { get; private set; }
+
+ ///
+ /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of
+ ///
+ /// "www.website.com/images/hello.png"
+ ///
+ /// included in the notification becomes
+ ///
+ /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us"
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ ///
+ /// Gets or sets a default base URI that is combined with relative URIs in image source attributes.
+ ///
+ public Uri BaseUri { get; set; }
+
+ ///
+ /// Gets or sets the form that the Tile should use to display the app's brand.
+ ///
+ public TileBranding Branding { get; set; } = DEFAULT_BRANDING;
+
+ ///
+ /// Gets or sets a sender-defined string that uniquely identifies the content of the notification. This prevents duplicates in the situation where a large Tile template is displaying the last three wide Tile notifications.
+ ///
+ /// Required: NO
+ ///
+ public string ContentId { get; set; }
+
+ ///
+ /// Gets or sets an optional string to override the Tile's display name while showing this notification.
+ ///
+ public string DisplayName { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides that in visual, but can be overridden by that in text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. See Remarks for when this value isn't specified.
+ ///
+ public string Language { get; set; }
+
+ public string LockDetailedStatus1 { get; set; }
+
+ public string LockDetailedStatus2 { get; set; }
+
+ public string LockDetailedStatus3 { get; set; }
+
+ public string Arguments { get; set; }
+
+ ///
+ /// Throws exception if value is invalid
+ ///
+ /// Overlay value (0-100)
+ internal static void CheckOverlayValue(int value)
+ {
+ if (value < 0 || value > 100)
+ {
+ throw new ArgumentOutOfRangeException("Overlay must be between 0 and 100, inclusive.");
+ }
+ }
+
+ public TilePresentation? Presentation { get; set; }
+
+ public TileTextStacking TextStacking { get; set; } = DEFAULT_TEXT_STACKING;
+
+ public IList Children { get; private set; } = new List();
+
+ ///
+ /// Generates an enumerable collection of children and all those children's children
+ ///
+ /// Enumerable collection of children and all those children's children.
+ public IEnumerable Descendants()
+ {
+ foreach (IElement_TileBindingChild child in Children)
+ {
+ // Return the child
+ yield return child;
+
+ // And if it has descendants, return the descendants
+ if (child is IElementWithDescendants)
+ {
+ foreach (object descendant in (child as IElementWithDescendants).Descendants())
+ {
+ yield return descendant;
+ }
+ }
+ }
+ }
+
+ ///
+ string IHaveXmlName.Name => "binding";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Children;
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("template", Template);
+ yield return new("addImageQuery", AddImageQuery);
+ yield return new("baseUri", BaseUri);
+
+ if (Branding != DEFAULT_BRANDING)
+ {
+ yield return new("branding", Branding);
+ }
+
+ yield return new("contentId", ContentId);
+ yield return new("displayName", DisplayName);
+ yield return new("lang", Language);
+ yield return new("hint-lockDetailedStatus1", LockDetailedStatus1);
+ yield return new("hint-lockDetailedStatus2", LockDetailedStatus2);
+ yield return new("hint-lockDetailedStatus3", LockDetailedStatus3);
+ yield return new("arguments", Arguments);
+ yield return new("hint-presentation", Presentation.ToPascalCaseString());
+
+ if (TextStacking != DEFAULT_TEXT_STACKING)
+ {
+ yield return new("hint-textStacking", TextStacking.ToPascalCaseString());
+ }
+ }
+ }
+
+ internal interface IElement_TileBindingChild
+ {
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/Elements/Element_TileVisual.cs b/components/Notifications/src/Tiles/Elements/Element_TileVisual.cs
new file mode 100644
index 000000000..d4a59a30f
--- /dev/null
+++ b/components/Notifications/src/Tiles/Elements/Element_TileVisual.cs
@@ -0,0 +1,54 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_TileVisual : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ internal const TileBranding DEFAULT_BRANDING = TileBranding.Auto;
+ internal const bool DEFAULT_ADD_IMAGE_QUERY = false;
+
+ public bool? AddImageQuery { get; set; }
+
+ public Uri BaseUri { get; set; }
+
+ public TileBranding Branding { get; set; } = DEFAULT_BRANDING;
+
+ public string ContentId { get; set; }
+
+ public string DisplayName { get; set; }
+
+ public string Language { get; set; }
+
+ public string Arguments { get; set; }
+
+ public IList Bindings { get; private set; } = new List();
+
+ ///
+ string IHaveXmlName.Name => "visual";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Bindings;
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("addImageQuery", AddImageQuery);
+ yield return new("baseUri", BaseUri);
+
+ if (Branding != DEFAULT_BRANDING)
+ {
+ yield return new("branding", Branding.ToPascalCaseString());
+ }
+
+ yield return new("contentId", ContentId);
+ yield return new("displayName", DisplayName);
+ yield return new("lang", Language);
+ yield return new("arguments", Arguments);
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/Elements/TileElementsCommon.cs b/components/Notifications/src/Tiles/Elements/TileElementsCommon.cs
new file mode 100644
index 000000000..f5b808fdd
--- /dev/null
+++ b/components/Notifications/src/Tiles/Elements/TileElementsCommon.cs
@@ -0,0 +1,20 @@
+// 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 CommunityToolkit.Notifications
+{
+ internal enum TilePresentation
+ {
+ People,
+ Photos,
+ Contact
+ }
+
+ internal enum TileImagePlacement
+ {
+ Inline,
+ Background,
+ Peek
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/ITileBindingContentAdaptiveChild.cs b/components/Notifications/src/Tiles/ITileBindingContentAdaptiveChild.cs
new file mode 100644
index 000000000..5c9ad93e8
--- /dev/null
+++ b/components/Notifications/src/Tiles/ITileBindingContentAdaptiveChild.cs
@@ -0,0 +1,14 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Elements that can be direct children of , including ( , , and ).
+ ///
+ public interface ITileBindingContentAdaptiveChild
+ {
+ // Blank interface simply for compile-enforcing the child types in the list.
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentContact.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentContact.cs
new file mode 100644
index 000000000..865bb849e
--- /dev/null
+++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentContact.cs
@@ -0,0 +1,43 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Phone-only. Supported on Small, Medium, and Wide.
+ ///
+ public sealed class TileBindingContentContact : ITileBindingContent
+ {
+ ///
+ /// Gets or sets the image to display.
+ ///
+ public TileBasicImage Image { get; set; }
+
+ ///
+ /// Gets or sets a line of text that is displayed. Not displayed on Small Tile.
+ ///
+ public TileBasicText Text { get; set; }
+
+ internal TileTemplateNameV3 GetTemplateName(TileSize size)
+ {
+ return TileSizeToAdaptiveTemplateConverter.Convert(size);
+ }
+
+ internal void PopulateElement(Element_TileBinding binding, TileSize size)
+ {
+ binding.Presentation = TilePresentation.Contact;
+
+ // Small size doesn't display the text, so no reason to include it in the payload
+ if (Text != null && size != TileSize.Small)
+ {
+ binding.Children.Add(Text.ConvertToElement());
+ }
+
+ if (Image != null)
+ {
+ binding.Children.Add(Image.ConvertToElement());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentIconic.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentIconic.cs
new file mode 100644
index 000000000..686278736
--- /dev/null
+++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentIconic.cs
@@ -0,0 +1,44 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Supported on Small and Medium. Enables an iconic Tile template, where you can have an icon and badge display next to each other on the Tile, in true classic Windows Phone style. The number next to the icon is achieved through a separate badge notification.
+ ///
+ public sealed class TileBindingContentIconic : ITileBindingContent
+ {
+ ///
+ /// Gets or sets, at minimum, to support both Desktop and Phone, Small and Medium tiles, a square aspect ratio image with a resolution of 200x200, PNG format, with transparency and no color other than white. For more info see: http://blogs.msdn.com/b/tiles_and_toasts/archive/2015/07/31/iconic-tile-template-for-windows-10.aspx
+ ///
+ public TileBasicImage Icon { get; set; }
+
+ internal TileTemplateNameV3 GetTemplateName(TileSize size)
+ {
+ switch (size)
+ {
+ case TileSize.Small:
+ return TileTemplateNameV3.TileSquare71x71IconWithBadge;
+
+ case TileSize.Medium:
+ return TileTemplateNameV3.TileSquare150x150IconWithBadge;
+
+ default:
+ throw new ArgumentException("The Iconic template is only supported on Small and Medium tiles.");
+ }
+ }
+
+ internal void PopulateElement(Element_TileBinding binding, TileSize size)
+ {
+ if (Icon != null)
+ {
+ var element = Icon.ConvertToElement();
+ element.Id = 1;
+ binding.Children.Add(element);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPeople.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPeople.cs
new file mode 100644
index 000000000..4f5fdcd42
--- /dev/null
+++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPeople.cs
@@ -0,0 +1,35 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// New in 1511: Supported on Medium, Wide, and Large (Desktop and Mobile).
+ /// Previously for RTM: Phone-only. Supported on Medium and Wide.
+ ///
+ public sealed class TileBindingContentPeople : ITileBindingContent
+ {
+ ///
+ /// Gets images that will roll around as circles.
+ ///
+ public IList Images { get; private set; } = new List();
+
+ internal TileTemplateNameV3 GetTemplateName(TileSize size)
+ {
+ return TileSizeToAdaptiveTemplateConverter.Convert(size);
+ }
+
+ internal void PopulateElement(Element_TileBinding binding, TileSize size)
+ {
+ binding.Presentation = TilePresentation.People;
+
+ foreach (var img in Images)
+ {
+ binding.Children.Add(img.ConvertToElement());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPhotos.cs b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPhotos.cs
new file mode 100644
index 000000000..baa5de892
--- /dev/null
+++ b/components/Notifications/src/Tiles/SpecialTemplates/TileBindingContentPhotos.cs
@@ -0,0 +1,34 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Animates through a slide show of photos. Supported on all sizes.
+ ///
+ public sealed class TileBindingContentPhotos : ITileBindingContent
+ {
+ ///
+ /// Gets the collection of slide show images. Up to 12 images can be provided (Mobile will only display up to 9), which will be used for the slide show. Adding more than 12 will throw an exception.
+ ///
+ public IList Images { get; private set; } = new LimitedList(12);
+
+ internal TileTemplateNameV3 GetTemplateName(TileSize size)
+ {
+ return TileSizeToAdaptiveTemplateConverter.Convert(size);
+ }
+
+ internal void PopulateElement(Element_TileBinding binding, TileSize size)
+ {
+ binding.Presentation = TilePresentation.Photos;
+
+ foreach (var img in Images)
+ {
+ binding.Children.Add(img.ConvertToElement());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileBackgroundImage.cs b/components/Notifications/src/Tiles/TileBackgroundImage.cs
new file mode 100644
index 000000000..b326d2825
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileBackgroundImage.cs
@@ -0,0 +1,90 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// A full-bleed background image that appears beneath the Tile content.
+ ///
+ public sealed class TileBackgroundImage : IBaseImage
+ {
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ public string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ private int? _hintOverlay;
+
+ ///
+ /// Gets or sets a black overlay on the background image. This value controls the opacity of the black overlay, with 0 being no overlay and 100 being completely black. Defaults to 20.
+ ///
+ public int? HintOverlay
+ {
+ get
+ {
+ return _hintOverlay;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ Element_TileBinding.CheckOverlayValue(value.Value);
+ }
+
+ _hintOverlay = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the desired cropping of the image.
+ /// Previously for RTM: Did not exist, value will be ignored and background image will be displayed without any cropping.
+ ///
+ public TileBackgroundImageCrop HintCrop { get; set; }
+
+ internal Element_AdaptiveImage ConvertToElement()
+ {
+ Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this);
+
+ image.Placement = AdaptiveImagePlacement.Background;
+ image.Crop = GetAdaptiveImageCrop();
+ image.Overlay = HintOverlay;
+
+ return image;
+ }
+
+ private AdaptiveImageCrop GetAdaptiveImageCrop()
+ {
+ switch (HintCrop)
+ {
+ case TileBackgroundImageCrop.Circle:
+ return AdaptiveImageCrop.Circle;
+
+ case TileBackgroundImageCrop.None:
+ return AdaptiveImageCrop.None;
+
+ default:
+ return AdaptiveImageCrop.Default;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileBasicImage.cs b/components/Notifications/src/Tiles/TileBasicImage.cs
new file mode 100644
index 000000000..c3b57b815
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileBasicImage.cs
@@ -0,0 +1,42 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// An image used on various special templates for the Tile.
+ ///
+ public sealed class TileBasicImage : IBaseImage
+ {
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ public string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ internal Element_AdaptiveImage ConvertToElement()
+ {
+ Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this);
+
+ return image;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileBasicText.cs b/components/Notifications/src/Tiles/TileBasicText.cs
new file mode 100644
index 000000000..465d1dd44
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileBasicText.cs
@@ -0,0 +1,42 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// A text element on the Tile.
+ ///
+ public sealed class TileBasicText
+ {
+ ///
+ /// Gets or sets the text value that will be shown in the text field.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ public string Lang { get; set; }
+
+ internal Element_AdaptiveText ConvertToElement()
+ {
+ return new Element_AdaptiveText()
+ {
+ Text = Text,
+ Lang = Lang
+ };
+ }
+
+ ///
+ /// Returns the Text property's value.
+ ///
+ /// The Text property's value.
+ public override string ToString()
+ {
+ return Text;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileBinding.cs b/components/Notifications/src/Tiles/TileBinding.cs
new file mode 100644
index 000000000..1290f47b2
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileBinding.cs
@@ -0,0 +1,168 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// The binding element contains the visual content for a specific Tile size.
+ ///
+ public sealed class TileBinding
+ {
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides that in visual, but can be overridden by that in text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. See Remarks for when this value isn't specified.
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Gets or sets a default base URI that is combined with relative URIs in image source attributes. Defaults to null.
+ ///
+ public Uri BaseUri { get; set; }
+
+ ///
+ /// Gets or sets the form that the Tile should use to display the app's brand..
+ ///
+ public TileBranding Branding { get; set; } = Element_TileBinding.DEFAULT_BRANDING;
+
+ ///
+ /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of
+ ///
+ /// "www.website.com/images/hello.png"
+ ///
+ /// included in the notification becomes
+ ///
+ /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us"
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ ///
+ /// Gets or sets a sender-defined string that uniquely identifies the content of the notification. This prevents duplicates in the situation where a large Tile template is displaying the last three wide Tile notifications.
+ ///
+ public string ContentId { get; set; }
+
+ ///
+ /// Gets or sets an optional string to override the Tile's display name while showing this notification.
+ ///
+ public string DisplayName { get; set; }
+
+ ///
+ /// Gets or sets an app-defined data that is passed back to your app via the TileActivatedInfo property on
+ /// LaunchActivatedEventArgs when the user launches your app from the Live Tile. This allows you to know
+ /// which Tile notifications your user saw when they tapped your Live Tile. On devices without the Anniversary Update,
+ /// this will simply be ignored.
+ ///
+ public string Arguments { get; set; }
+
+ ///
+ /// Gets or sets the actual content to be displayed. One of , , , , or
+ ///
+ public ITileBindingContent Content { get; set; }
+
+ internal Element_TileBinding ConvertToElement(TileSize size)
+ {
+ TileTemplateNameV3 templateName = GetTemplateName(Content, size);
+
+ Element_TileBinding binding = new Element_TileBinding(templateName)
+ {
+ Language = Language,
+ BaseUri = BaseUri,
+ Branding = Branding,
+ AddImageQuery = AddImageQuery,
+ DisplayName = DisplayName,
+ ContentId = ContentId,
+ Arguments = Arguments
+
+ // LockDetailedStatus gets populated by TileVisual
+ };
+
+ PopulateElement(Content, binding, size);
+
+ return binding;
+ }
+
+ private static void PopulateElement(ITileBindingContent bindingContent, Element_TileBinding binding, TileSize size)
+ {
+ if (bindingContent == null)
+ {
+ return;
+ }
+
+ if (bindingContent is TileBindingContentAdaptive)
+ {
+ (bindingContent as TileBindingContentAdaptive).PopulateElement(binding, size);
+ }
+ else if (bindingContent is TileBindingContentContact)
+ {
+ (bindingContent as TileBindingContentContact).PopulateElement(binding, size);
+ }
+ else if (bindingContent is TileBindingContentIconic)
+ {
+ (bindingContent as TileBindingContentIconic).PopulateElement(binding, size);
+ }
+ else if (bindingContent is TileBindingContentPeople)
+ {
+ (bindingContent as TileBindingContentPeople).PopulateElement(binding, size);
+ }
+ else if (bindingContent is TileBindingContentPhotos)
+ {
+ (bindingContent as TileBindingContentPhotos).PopulateElement(binding, size);
+ }
+ else
+ {
+ throw new NotImplementedException("Unknown binding content type: " + bindingContent.GetType());
+ }
+ }
+
+ private static TileTemplateNameV3 GetTemplateName(ITileBindingContent bindingContent, TileSize size)
+ {
+ if (bindingContent == null)
+ {
+ return TileSizeToAdaptiveTemplateConverter.Convert(size);
+ }
+
+ if (bindingContent is TileBindingContentAdaptive)
+ {
+ return (bindingContent as TileBindingContentAdaptive).GetTemplateName(size);
+ }
+
+ if (bindingContent is TileBindingContentContact)
+ {
+ return (bindingContent as TileBindingContentContact).GetTemplateName(size);
+ }
+
+ if (bindingContent is TileBindingContentIconic)
+ {
+ return (bindingContent as TileBindingContentIconic).GetTemplateName(size);
+ }
+
+ if (bindingContent is TileBindingContentPeople)
+ {
+ return (bindingContent as TileBindingContentPeople).GetTemplateName(size);
+ }
+
+ if (bindingContent is TileBindingContentPhotos)
+ {
+ return (bindingContent as TileBindingContentPhotos).GetTemplateName(size);
+ }
+
+ throw new NotImplementedException("Unknown binding content type: " + bindingContent.GetType());
+ }
+ }
+
+ ///
+ /// Visual Tile content. One of , , , , or .
+ ///
+ public interface ITileBindingContent
+ {
+ }
+
+ internal enum TileTemplate
+ {
+ TileSmall,
+ TileMedium,
+ TileWide,
+ TileLarge
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileBindingContentAdaptive.cs b/components/Notifications/src/Tiles/TileBindingContentAdaptive.cs
new file mode 100644
index 000000000..3b970165e
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileBindingContentAdaptive.cs
@@ -0,0 +1,72 @@
+// 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.Collections.Generic;
+using CommunityToolkit.Notifications.Adaptive;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Supported on all sizes. This is the recommended way of specifying your Tile content. Adaptive Tile templates are the de-facto choice for Windows 10, and you can create a wide variety of custom Tiles through adaptive.
+ ///
+ public sealed class TileBindingContentAdaptive : ITileBindingContent
+ {
+ ///
+ /// Gets , , and objects that can be added as children. The children are displayed in a vertical StackPanel fashion.
+ ///
+ public IList Children { get; private set; } = new List();
+
+ ///
+ /// Gets or sets an optional background image that gets displayed behind all the Tile content, full bleed.
+ ///
+ public TileBackgroundImage BackgroundImage { get; set; }
+
+ ///
+ /// Gets or sets an optional peek image that animates in from the top of the Tile.
+ ///
+ public TilePeekImage PeekImage { get; set; }
+
+ ///
+ /// Gets or sets the text stacking (vertical alignment) of the entire binding element.
+ ///
+ public TileTextStacking TextStacking { get; set; } = Element_TileBinding.DEFAULT_TEXT_STACKING;
+
+ internal TileTemplateNameV3 GetTemplateName(TileSize size)
+ {
+ return TileSizeToAdaptiveTemplateConverter.Convert(size);
+ }
+
+ internal void PopulateElement(Element_TileBinding binding, TileSize size)
+ {
+ // Assign properties
+ binding.TextStacking = TextStacking;
+
+ // Add the background image if there's one
+ if (BackgroundImage != null)
+ {
+ // And add it as a child
+ binding.Children.Add(BackgroundImage.ConvertToElement());
+ }
+
+ // Add the peek image if there's one
+ if (PeekImage != null)
+ {
+ var el = PeekImage.ConvertToElement();
+
+ binding.Children.Add(el);
+ }
+
+ // And then add all the children
+ foreach (var child in Children)
+ {
+ binding.Children.Add(ConvertToBindingChildElement(child));
+ }
+ }
+
+ private static IElement_TileBindingChild ConvertToBindingChildElement(ITileBindingContentAdaptiveChild child)
+ {
+ return (IElement_TileBindingChild)AdaptiveHelper.ConvertToElement(child);
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileBranding.cs b/components/Notifications/src/Tiles/TileBranding.cs
new file mode 100644
index 000000000..c70a50c0b
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileBranding.cs
@@ -0,0 +1,37 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// The form that the Tile should use to display the app's brand.
+ ///
+ public enum TileBranding
+ {
+ ///
+ /// The default choice. If ShowNameOn___ is true for the Tile size being displayed, then branding will be "Name". Otherwise it will be "None".
+ ///
+ Auto,
+
+ ///
+ /// No branding will be displayed.
+ ///
+ None,
+
+ ///
+ /// The DisplayName will be shown.
+ ///
+ Name,
+
+ ///
+ /// Desktop-only. The Square44x44Logo will be shown. On Mobile, this will fallback to Name.
+ ///
+ Logo,
+
+ ///
+ /// Desktop-only. Both the DisplayName and Square44x44Logo will be shown. On Mobile, this will fallback to Name.
+ ///
+ NameAndLogo
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileCommon.cs b/components/Notifications/src/Tiles/TileCommon.cs
new file mode 100644
index 000000000..96544d3c9
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileCommon.cs
@@ -0,0 +1,35 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Represent the all tile sizes that are available.
+ ///
+ [Flags]
+ public enum TileSize
+ {
+ ///
+ /// Small Square Tile
+ ///
+ Small = 1,
+
+ ///
+ /// Medium Square Tile
+ ///
+ Medium = 2,
+
+ ///
+ /// Wide Rectangle Tile
+ ///
+ Wide = 4,
+
+ ///
+ /// Large Square Tile
+ ///
+ Large = 8
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileContent.cs b/components/Notifications/src/Tiles/TileContent.cs
new file mode 100644
index 000000000..0f4608809
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileContent.cs
@@ -0,0 +1,53 @@
+// 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 Windows.Data.Xml.Dom;
+using Windows.UI.Notifications;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Base Tile element, which contains a single visual element.
+ ///
+ public sealed class TileContent
+ {
+ ///
+ /// Gets or sets the visual element. Required.
+ ///
+ public TileVisual Visual { get; set; }
+
+ ///
+ /// Retrieves the notification XML content as a string, so that it can be sent with a HTTP POST in a push notification.
+ ///
+ /// The notification XML content as a string.
+ public string GetContent()
+ {
+ return ConvertToElement().GetContent();
+ }
+
+ ///
+ /// Retrieves the notification XML content as a WinRT XmlDocument, so that it can be used with a local Tile notification's constructor on either or .
+ ///
+ /// The notification XML content as a WinRT XmlDocument.
+ public XmlDocument GetXml()
+ {
+ XmlDocument doc = new XmlDocument();
+ doc.LoadXml(GetContent());
+
+ return doc;
+ }
+
+ internal Element_Tile ConvertToElement()
+ {
+ var tile = new Element_Tile();
+
+ if (Visual != null)
+ {
+ tile.Visual = Visual.ConvertToElement();
+ }
+
+ return tile;
+ }
+ }
+}
diff --git a/components/Notifications/src/Tiles/TileImages.cs b/components/Notifications/src/Tiles/TileImages.cs
new file mode 100644
index 000000000..6ac9407f1
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileImages.cs
@@ -0,0 +1,48 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Specify the desired cropping of the image.
+ ///
+ public enum TileBackgroundImageCrop
+ {
+ ///
+ /// Cropping style automatically determined by renderer.
+ ///
+ Default,
+
+ ///
+ /// Default value. Image is not cropped.
+ ///
+ None,
+
+ ///
+ /// Image is cropped to a circle shape.
+ ///
+ Circle
+ }
+
+ ///
+ /// Specify the desired cropping of the image.
+ ///
+ public enum TilePeekImageCrop
+ {
+ ///
+ /// Cropping style automatically determined by renderer.
+ ///
+ Default,
+
+ ///
+ /// Default value. Image is not cropped.
+ ///
+ None,
+
+ ///
+ /// Image is cropped to a circle shape.
+ ///
+ Circle
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TilePeekImage.cs b/components/Notifications/src/Tiles/TilePeekImage.cs
new file mode 100644
index 000000000..e8e75f309
--- /dev/null
+++ b/components/Notifications/src/Tiles/TilePeekImage.cs
@@ -0,0 +1,91 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// A peek image that animates in from the top of the Tile.
+ ///
+ public sealed class TilePeekImage : IBaseImage
+ {
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ public string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets set a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ private int? _hintOverlay;
+
+ ///
+ /// Gets or sets a black overlay on the peek image. This value controls the opacity of the black overlay, with 0 being no overlay and 100 being completely black. Defaults to 0.
+ /// Previously for RTM: Did not exist, value will be ignored and peek image will be displayed with 0 overlay.
+ ///
+ public int? HintOverlay
+ {
+ get
+ {
+ return _hintOverlay;
+ }
+
+ set
+ {
+ if (value != null)
+ {
+ Element_TileBinding.CheckOverlayValue(value.Value);
+ }
+
+ _hintOverlay = value;
+ }
+ }
+
+ ///
+ /// Gets or sets the desired cropping of the image.
+ /// Previously for RTM: Did not exist, value will be ignored and peek image will be displayed without any cropping.
+ ///
+ public TilePeekImageCrop HintCrop { get; set; }
+
+ internal Element_AdaptiveImage ConvertToElement()
+ {
+ Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this);
+
+ image.Placement = AdaptiveImagePlacement.Peek;
+ image.Crop = GetAdaptiveImageCrop();
+ image.Overlay = HintOverlay;
+
+ return image;
+ }
+
+ private AdaptiveImageCrop GetAdaptiveImageCrop()
+ {
+ switch (HintCrop)
+ {
+ case TilePeekImageCrop.Circle:
+ return AdaptiveImageCrop.Circle;
+
+ case TilePeekImageCrop.None:
+ return AdaptiveImageCrop.None;
+
+ default:
+ return AdaptiveImageCrop.Default;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileSizeToAdaptiveTemplateConverter.cs b/components/Notifications/src/Tiles/TileSizeToAdaptiveTemplateConverter.cs
new file mode 100644
index 000000000..86b19981d
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileSizeToAdaptiveTemplateConverter.cs
@@ -0,0 +1,32 @@
+// 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 CommunityToolkit.Notifications
+{
+ internal static class TileSizeToAdaptiveTemplateConverter
+ {
+ public static TileTemplateNameV3 Convert(TileSize size)
+ {
+ switch (size)
+ {
+ case TileSize.Small:
+ return TileTemplateNameV3.TileSmall;
+
+ case TileSize.Medium:
+ return TileTemplateNameV3.TileMedium;
+
+ case TileSize.Wide:
+ return TileTemplateNameV3.TileWide;
+
+ case TileSize.Large:
+ return TileTemplateNameV3.TileLarge;
+
+ default:
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileTemplateNameV3.cs b/components/Notifications/src/Tiles/TileTemplateNameV3.cs
new file mode 100644
index 000000000..1cdd685aa
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileTemplateNameV3.cs
@@ -0,0 +1,92 @@
+// 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 CommunityToolkit.Notifications
+{
+ internal enum TileTemplateNameV3
+ {
+ TileMedium,
+ TileSmall,
+ TileWide,
+ TileLarge,
+
+ TileSquare150x150Block,
+ TileSquare150x150Image,
+ TileSquare150x150PeekImageAndText01,
+ TileSquare150x150PeekImageAndText02,
+ TileSquare150x150PeekImageAndText03,
+ TileSquare150x150PeekImageAndText04,
+ TileSquare150x150Text01,
+ TileSquare150x150Text02,
+ TileSquare150x150Text03,
+ TileSquare150x150Text04,
+ TileSquare310x310BlockAndText01,
+ TileSquare310x310BlockAndText02,
+ TileSquare310x310Image,
+ TileSquare310x310ImageAndText01,
+ TileSquare310x310ImageAndText02,
+ TileSquare310x310ImageAndTextOverlay01,
+ TileSquare310x310ImageAndTextOverlay02,
+ TileSquare310x310ImageAndTextOverlay03,
+ TileSquare310x310ImageCollection,
+ TileSquare310x310ImageCollectionAndText01,
+ TileSquare310x310ImageCollectionAndText02,
+ TileSquare310x310SmallImagesAndTextList01,
+ TileSquare310x310SmallImagesAndTextList02,
+ TileSquare310x310SmallImagesAndTextList03,
+ TileSquare310x310SmallImagesAndTextList04,
+ TileSquare310x310Text01,
+ TileSquare310x310Text02,
+ TileSquare310x310Text03,
+ TileSquare310x310Text04,
+ TileSquare310x310Text05,
+ TileSquare310x310Text06,
+ TileSquare310x310Text07,
+ TileSquare310x310Text08,
+ TileSquare310x310TextList01,
+ TileSquare310x310TextList02,
+ TileSquare310x310TextList03,
+ TileWide310x150BlockAndText01,
+ TileWide310x150BlockAndText02,
+ TileWide310x150Image,
+ TileWide310x150ImageAndText01,
+ TileWide310x150ImageAndText02,
+ TileWide310x150ImageCollection,
+ TileWide310x150PeekImage01,
+ TileWide310x150PeekImage02,
+ TileWide310x150PeekImage03,
+ TileWide310x150PeekImage04,
+ TileWide310x150PeekImage05,
+ TileWide310x150PeekImage06,
+ TileWide310x150PeekImageAndText01,
+ TileWide310x150PeekImageAndText02,
+ TileWide310x150PeekImageCollection01,
+ TileWide310x150PeekImageCollection02,
+ TileWide310x150PeekImageCollection03,
+ TileWide310x150PeekImageCollection04,
+ TileWide310x150PeekImageCollection05,
+ TileWide310x150PeekImageCollection06,
+ TileWide310x150SmallImageAndText01,
+ TileWide310x150SmallImageAndText02,
+ TileWide310x150SmallImageAndText03,
+ TileWide310x150SmallImageAndText04,
+ TileWide310x150SmallImageAndText05,
+ TileWide310x150Text01,
+ TileWide310x150Text02,
+ TileWide310x150Text03,
+ TileWide310x150Text04,
+ TileWide310x150Text05,
+ TileWide310x150Text06,
+ TileWide310x150Text07,
+ TileWide310x150Text08,
+ TileWide310x150Text09,
+ TileWide310x150Text10,
+ TileWide310x150Text11,
+
+ TileSquare71x71Image,
+ TileSquare71x71IconWithBadge,
+ TileSquare150x150IconWithBadge,
+ TileWide310x150IconWithBadgeAndText
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileTextStacking.cs b/components/Notifications/src/Tiles/TileTextStacking.cs
new file mode 100644
index 000000000..4b4e352dd
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileTextStacking.cs
@@ -0,0 +1,27 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// TextStacking specifies the vertical alignment of content.
+ ///
+ public enum TileTextStacking
+ {
+ ///
+ /// Vertical align to the top.
+ ///
+ Top,
+
+ ///
+ /// Vertical align to the center.
+ ///
+ Center,
+
+ ///
+ /// Vertical align to the bottom.
+ ///
+ Bottom
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Tiles/TileVisual.cs b/components/Notifications/src/Tiles/TileVisual.cs
new file mode 100644
index 000000000..123a785c3
--- /dev/null
+++ b/components/Notifications/src/Tiles/TileVisual.cs
@@ -0,0 +1,219 @@
+// 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.Linq;
+using CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Contains multiple binding child elements, each of which defines a Tile.
+ ///
+ public sealed class TileVisual
+ {
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Gets or sets a default base URI that is combined with relative URIs in image source attributes.
+ ///
+ public Uri BaseUri { get; set; }
+
+ ///
+ /// Gets or sets the form that the Tile should use to display the app's brand.
+ ///
+ public TileBranding Branding { get; set; } = Element_TileVisual.DEFAULT_BRANDING;
+
+ ///
+ /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of
+ ///
+ /// "www.website.com/images/hello.png"
+ ///
+ /// included in the notification becomes
+ ///
+ /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us"
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ ///
+ /// Gets or sets a sender-defined string that uniquely identifies the content of the notification. This prevents duplicates in the situation where a large Tile template is displaying the last three wide Tile notifications.
+ ///
+ public string ContentId { get; set; }
+
+ ///
+ /// Gets or sets an optional string to override the Tile's display name while showing this notification.
+ ///
+ public string DisplayName { get; set; }
+
+ ///
+ /// Gets or sets the first line of text that will be displayed on the lock screen if the user has selected
+ /// your Tile as their detailed status app. Ff you specify this, you must also provide a Wide Tile binding.
+ ///
+ public string LockDetailedStatus1 { get; set; }
+
+ ///
+ /// Gets or sets the second line of text that will be displayed on the lock screen if the user has selected
+ /// your Tile as their detailed status app. If you specify this, you must also provide a Wide Tile binding.
+ ///
+ public string LockDetailedStatus2 { get; set; }
+
+ ///
+ /// Gets or sets the third line of text that will be displayed on the lock screen if the user has selected your
+ /// Tile as their detailed status app. If you specify this, you must also provide a Wide Tile binding.
+ ///
+ public string LockDetailedStatus3 { get; set; }
+
+ ///
+ /// Gets or sets app-defined data that is passed back to your app via the TileActivatedInfo property on LaunchActivatedEventArgs when the user launches your app from the Live Tile. This allows you to know which Tile notifications your user saw when they tapped your Live Tile. On devices without the Anniversary Update, this will simply be ignored.
+ ///
+ public string Arguments { get; set; }
+
+ ///
+ /// Gets or sets an optional small binding to specify content for the small Tile size.
+ ///
+ public TileBinding TileSmall { get; set; }
+
+ ///
+ /// Gets or sets an optional medium binding to specify content for the medium Tile size.
+ ///
+ public TileBinding TileMedium { get; set; }
+
+ ///
+ /// Gets or sets an optional wide binding to specify content for the wide Tile size.
+ ///
+ public TileBinding TileWide { get; set; }
+
+ ///
+ /// Gets or sets an optional large binding to specify content for the large Tile size. Desktop-only
+ ///
+ public TileBinding TileLarge { get; set; }
+
+ ///
+ /// Attempts to find and re-use an existing text element inside the binding. Returns true if it could. Otherwise returns false, and the caller will have to specify the detailed status using the lock hint attribute.
+ ///
+ /// The lock screen line number.
+ /// The lock screen line text.
+ /// The binding to look in for matches.
+ /// True if could re-use existing text element, otherwise false.
+ private static bool TryReuseTextElementForLockDetailedText(int lineNumber, string lockText, Element_TileBinding binding)
+ {
+ if (lockText == null)
+ {
+ throw new ArgumentNullException("lockText cannot be null");
+ }
+
+ if (binding == null)
+ {
+ throw new ArgumentNullException("binding cannot be null");
+ }
+
+ // If a text element already has an id with the line number (only look at immediate children, since the lock screen will ignore things under groups/subgroups)
+ Element_AdaptiveText matchingIdTextElement = binding.Children.OfType().FirstOrDefault(i => i.Id != null && i.Id.Equals(lineNumber.ToString()));
+
+ if (matchingIdTextElement != null)
+ {
+ // If the text in the element matches the lock text, then we're good, don't need to assign anything else!
+ if (matchingIdTextElement.Text != null && matchingIdTextElement.Text.Equals(lockText))
+ {
+ return true;
+ }
+
+ // Otherwise, we need to specify the lock text in the hint attribute, so we return false
+ return false;
+ }
+
+ // Otherwise no text elements use that ID, so we could assign one if we find a text element that doesn't have an ID assigned and matches the lock text
+ Element_AdaptiveText matchingTextTextElement = binding.Children.OfType().FirstOrDefault(i => i.Id == null && i.Text != null && i.Text.Equals(lockText));
+
+ // If we found text that matched, we'll assign the id so it gets re-used for lock!
+ if (matchingTextTextElement != null)
+ {
+ matchingTextTextElement.Id = lineNumber;
+ return true;
+ }
+
+ // Otherwise we'll need to specify lock text in hint attribute, so return false
+ return false;
+ }
+
+ internal Element_TileVisual ConvertToElement()
+ {
+ var visual = new Element_TileVisual()
+ {
+ Language = Language,
+ BaseUri = BaseUri,
+ Branding = Branding,
+ AddImageQuery = AddImageQuery,
+ ContentId = ContentId,
+ DisplayName = DisplayName,
+ Arguments = Arguments
+ };
+
+ if (TileSmall != null)
+ {
+ visual.Bindings.Add(TileSmall.ConvertToElement(TileSize.Small));
+ }
+
+ if (TileMedium != null)
+ {
+ visual.Bindings.Add(TileMedium.ConvertToElement(TileSize.Medium));
+ }
+
+ if (TileWide != null)
+ {
+ Element_TileBinding wideBindingElement = TileWide.ConvertToElement(TileSize.Wide);
+
+ // If lock detailed status was specified, add them
+ if (LockDetailedStatus1 != null)
+ {
+ // If we can't reuse existing text element, we'll have to use the hints
+ if (!TryReuseTextElementForLockDetailedText(1, LockDetailedStatus1, wideBindingElement))
+ {
+ wideBindingElement.LockDetailedStatus1 = LockDetailedStatus1;
+ }
+ }
+
+ if (LockDetailedStatus2 != null)
+ {
+ if (!TryReuseTextElementForLockDetailedText(2, LockDetailedStatus2, wideBindingElement))
+ {
+ wideBindingElement.LockDetailedStatus2 = LockDetailedStatus2;
+ }
+ }
+
+ if (LockDetailedStatus3 != null)
+ {
+ if (!TryReuseTextElementForLockDetailedText(3, LockDetailedStatus3, wideBindingElement))
+ {
+ wideBindingElement.LockDetailedStatus3 = LockDetailedStatus3;
+ }
+ }
+
+ visual.Bindings.Add(wideBindingElement);
+ }
+
+ // Otherwise if they specified lock values, throw an exception since lock values require wide
+ else if (HasLockDetailedStatusValues())
+ {
+ throw new Exception("To provide lock detailed status text strings, you must also provide a TileWide binding. Either provide a TileWide binding, or leave the detailed status values null.");
+ }
+
+ if (TileLarge != null)
+ {
+ visual.Bindings.Add(TileLarge.ConvertToElement(TileSize.Large));
+ }
+
+ // TODO: If a BaseUri wasn't provided, we can potentially optimize the payload size by calculating the best BaseUri
+ return visual;
+ }
+
+ private bool HasLockDetailedStatusValues()
+ {
+ return LockDetailedStatus1 != null && LockDetailedStatus2 != null && LockDetailedStatus3 != null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Builder/CustomizeToast.cs b/components/Notifications/src/Toasts/Builder/CustomizeToast.cs
new file mode 100644
index 000000000..1f4bf30bb
--- /dev/null
+++ b/components/Notifications/src/Toasts/Builder/CustomizeToast.cs
@@ -0,0 +1,35 @@
+// 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 Windows.Foundation;
+using Windows.UI.Notifications;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Allows you to set additional properties on the object before the toast is displayed.
+ ///
+ /// The toast to modify that will be displayed.
+ public delegate void CustomizeToast(ToastNotification toast);
+
+ ///
+ /// Allows you to set additional properties on the object before the toast is displayed.
+ ///
+ /// The toast to modify that will be displayed.
+ /// An operation.
+ public delegate IAsyncAction CustomizeToastAsync(ToastNotification toast);
+
+ ///
+ /// Allows you to set additional properties on the object before the toast is scheduled.
+ ///
+ /// The scheduled toast to modify that will be scheduled.
+ public delegate void CustomizeScheduledToast(ScheduledToastNotification toast);
+
+ ///
+ /// Allows you to set additional properties on the object before the toast is scheduled.
+ ///
+ /// The scheduled toast to modify that will be scheduled.
+ /// An operation.
+ public delegate IAsyncAction CustomizeScheduledToastAsync(ScheduledToastNotification toast);
+}
diff --git a/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Actions.cs b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Actions.cs
new file mode 100644
index 000000000..2bfc96d06
--- /dev/null
+++ b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Actions.cs
@@ -0,0 +1,282 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+#pragma warning disable SA1008
+#pragma warning disable SA1009
+ ///
+ /// Builder class used to create
+ ///
+ public partial class ToastContentBuilder
+ {
+ private IToastActions Actions
+ {
+ get
+ {
+ if (Content.Actions == null)
+ {
+ Content.Actions = new ToastActionsCustom();
+ }
+
+ return Content.Actions;
+ }
+ }
+
+ private IList ButtonList
+ {
+ get
+ {
+ return ((ToastActionsCustom)Actions).Buttons;
+ }
+ }
+
+ private IList InputList
+ {
+ get
+ {
+ return ((ToastActionsCustom)Actions).Inputs;
+ }
+ }
+
+ 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();
+ }
+
+ ///
+ /// 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 string of 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 content, ToastActivationType activationType, string arguments)
+ {
+ return AddButton(content, activationType, arguments, default);
+ }
+
+ ///
+ /// 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 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
+ public ToastContentBuilder AddButton(string content, ToastActivationType activationType, string arguments, Uri imageUri)
+ {
+ // Add new button
+ ToastButton button = new ToastButton(content, arguments)
+ {
+ ActivationType = activationType
+ };
+
+ if (imageUri != default)
+ {
+ button.ImageUri = imageUri.OriginalString;
+ }
+
+ return AddButton(button);
+ }
+
+ ///
+ /// Add a button to the current toast.
+ ///
+ /// An instance of class that implement for the button that will be used on the toast.
+ /// The current instance of
+ public ToastContentBuilder AddButton(IToastButton button)
+ {
+ if (button is ToastButton toastButton && toastButton.Content == null && toastButton.NeedsContent())
+ {
+ 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");
+ }
+
+ if (button is ToastButton b && b.CanAddArguments())
+ {
+ foreach (var arg in _genericArguments)
+ {
+ if (!b.ContainsArgument(arg.Key))
+ {
+ b.AddArgument(arg.Key, arg.Value);
+ }
+ }
+ }
+
+ ButtonList.Add(button);
+
+ return this;
+ }
+
+ ///
+ /// 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 string of 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, string arguments)
+ {
+ return AddButton(textBoxId, 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 string of 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
+ public ToastContentBuilder AddButton(string textBoxId, string content, ToastActivationType activationType, string arguments, Uri imageUri)
+ {
+ // Add new button
+ ToastButton button = new ToastButton(content, arguments)
+ {
+ ActivationType = activationType,
+ TextBoxId = textBoxId
+ };
+
+ if (imageUri != default)
+ {
+ button.ImageUri = imageUri.OriginalString;
+ }
+
+ return AddButton(button);
+ }
+
+ ///
+ /// Add an input text box that the user can type into.
+ ///
+ /// Required ID property so that developers can retrieve user input once the app is activated.
+ /// Placeholder text to be displayed on the text box when the user hasn't typed any text yet.
+ /// Title text to display above the text box.
+ /// The current instance of
+ public ToastContentBuilder AddInputTextBox(
+ string id,
+ string placeHolderContent = default,
+ string title = default)
+ {
+ var inputTextBox = new ToastTextBox(id);
+
+ if (placeHolderContent != default)
+ {
+ inputTextBox.PlaceholderContent = placeHolderContent;
+ }
+
+ if (title != default)
+ {
+ inputTextBox.Title = title;
+ }
+
+ return AddToastInput(inputTextBox);
+ }
+
+ ///
+ /// Add a combo box / drop-down menu that contain options for user to select.
+ ///
+ /// Required ID property used so that developers can retrieve user input once the app is activated.
+ /// List of choices that will be available for user to select.
+ /// The current instance of
+ public ToastContentBuilder AddComboBox(string id, params (string comboBoxItemId, string comboBoxItemContent)[] choices)
+ {
+ return AddComboBox(id, default, choices);
+ }
+
+ ///
+ /// Add a combo box / drop-down menu that contain options for user to select.
+ ///
+ /// Required ID property used so that developers can retrieve user input once the app is activated.
+ /// Sets which item is selected by default, and refers to the Id property of . If you do not provide this or null, the default selection will be empty (user sees nothing).
+ /// List of choices that will be available for user to select.
+ /// The current instance of
+ public ToastContentBuilder AddComboBox(string id, string defaultSelectionBoxItemId, params (string comboBoxItemId, string comboBoxItemContent)[] choices)
+ {
+ return AddComboBox(id, default, defaultSelectionBoxItemId, choices);
+ }
+
+ ///
+ /// Add a combo box / drop-down menu that contain options for user to select.
+ ///
+ /// Required ID property used so that developers can retrieve user input once the app is activated.
+ /// Title text to display above the Combo Box.
+ /// Sets which item is selected by default, and refers to the Id property of . If you do not provide this or null, the default selection will be empty (user sees nothing).
+ /// List of choices that will be available for user to select.
+ /// The current instance of
+ public ToastContentBuilder AddComboBox(string id, string title, string defaultSelectionBoxItemId, params (string comboBoxItemId, string comboBoxItemContent)[] choices)
+ {
+ return AddComboBox(id, title, defaultSelectionBoxItemId, choices as IEnumerable<(string, string)>);
+ }
+
+ ///
+ /// Add a combo box / drop-down menu that contain options for user to select.
+ ///
+ /// Required ID property used so that developers can retrieve user input once the app is activated.
+ /// Title text to display above the Combo Box.
+ /// Sets which item is selected by default, and refers to the Id property of . If you do not provide this or null, the default selection will be empty (user sees nothing).
+ /// List of choices that will be available for user to select.
+ /// The current instance of
+ public ToastContentBuilder AddComboBox(string id, string title, string defaultSelectionBoxItemId, IEnumerable<(string comboBoxItemId, string comboBoxItemContent)> choices)
+ {
+ var box = new ToastSelectionBox(id);
+
+ if (defaultSelectionBoxItemId != default)
+ {
+ box.DefaultSelectionBoxItemId = defaultSelectionBoxItemId;
+ }
+
+ if (title != default)
+ {
+ box.Title = title;
+ }
+
+ for (int i = 0; i < choices.Count(); i++)
+ {
+ var (comboBoxItemId, comboBoxItemContent) = choices.ElementAt(i);
+ box.Items.Add(new ToastSelectionBoxItem(comboBoxItemId, comboBoxItemContent));
+ }
+
+ return AddToastInput(box);
+ }
+
+ ///
+ /// Add an input option to the Toast.
+ ///
+ /// An instance of a class that implement that will be used on the toast.
+ /// The current instance of
+ public ToastContentBuilder AddToastInput(IToastInput input)
+ {
+ InputList.Add(input);
+
+ return this;
+ }
+ }
+#pragma warning restore SA1008
+#pragma warning restore SA1009
+}
diff --git a/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Visuals.cs b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Visuals.cs
new file mode 100644
index 000000000..1e3ce72d9
--- /dev/null
+++ b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.Visuals.cs
@@ -0,0 +1,419 @@
+// 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 System.Text;
+using Windows.UI.Notifications;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Builder class used to create
+ ///
+ public partial class ToastContentBuilder
+ {
+ private ToastVisual Visual
+ {
+ get
+ {
+ if (Content.Visual == null)
+ {
+ Content.Visual = new ToastVisual();
+ Content.Visual.BindingGeneric = new ToastBindingGeneric();
+ }
+
+ return Content.Visual;
+ }
+ }
+
+ private ToastGenericAppLogo AppLogoOverrideUri
+ {
+ get
+ {
+ return Visual.BindingGeneric.AppLogoOverride;
+ }
+
+ set
+ {
+ Visual.BindingGeneric.AppLogoOverride = value;
+ }
+ }
+
+ private ToastGenericAttributionText AttributionText
+ {
+ get
+ {
+ return Visual.BindingGeneric.Attribution;
+ }
+
+ set
+ {
+ Visual.BindingGeneric.Attribution = value;
+ }
+ }
+
+ private ToastGenericHeroImage HeroImage
+ {
+ get
+ {
+ return Visual.BindingGeneric.HeroImage;
+ }
+
+ set
+ {
+ Visual.BindingGeneric.HeroImage = value;
+ }
+ }
+
+ private IList VisualChildren
+ {
+ get
+ {
+ return Visual.BindingGeneric.Children;
+ }
+ }
+
+ ///
+ /// Create an instance of NotificationData that can be used to update toast that has a progress bar.
+ ///
+ /// Instance of ToastContent that contain progress bars that need to be updated
+ /// Index of the progress bar (0-based) that this notification data is updating in the case that toast has multiple progress bars. Default to 0.
+ /// Title of the progress bar.
+ /// Value of the progress bar.
+ /// An optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed.
+ /// A status string, which is displayed underneath the progress bar on the left. Default to empty.
+ /// A sequence number to prevent out-of-order updates, or assign 0 to indicate "always update".
+ /// An instance of NotificationData that can be used to update the toast.
+ public static NotificationData CreateProgressBarData(ToastContent toast, int index = 0, string title = default, double? value = default, string valueStringOverride = default, string status = default, uint sequence = 0)
+ {
+ var progressBar = toast.Visual.BindingGeneric.Children.Where(c => c is AdaptiveProgressBar).ElementAt(index) as AdaptiveProgressBar;
+ if (progressBar == null)
+ {
+ throw new ArgumentException(nameof(toast), "Given toast does not have any progress bar");
+ }
+
+ NotificationData data = new NotificationData();
+ data.SequenceNumber = sequence;
+
+ // Native C++ doesn't support BindableString
+ if (progressBar.Title is BindableString bindableTitle && title != default)
+ {
+ data.Values[bindableTitle.BindingName] = title;
+ }
+
+ if (progressBar.Value is BindableProgressBarValue bindableProgressValue && value != default)
+ {
+ data.Values[bindableProgressValue.BindingName] = value.ToString();
+ }
+
+ if (progressBar.ValueStringOverride is BindableString bindableValueStringOverride && valueStringOverride != default)
+ {
+ data.Values[bindableValueStringOverride.BindingName] = valueStringOverride;
+ }
+
+ if (progressBar.Status is BindableString bindableStatus && status != default)
+ {
+ data.Values[bindableStatus.BindingName] = status;
+ }
+
+ return data;
+ }
+
+ ///
+ /// Add an Attribution Text to be displayed on the toast.
+ ///
+ /// Text to be displayed as Attribution Text
+ /// The current instance of
+ public ToastContentBuilder AddAttributionText(string text)
+ {
+ return AddAttributionText(text, default);
+ }
+
+ ///
+ /// Add an Attribution Text to be displayed on the toast.
+ ///
+ /// Text to be displayed as Attribution Text
+ /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR".
+ /// The current instance of
+ public ToastContentBuilder AddAttributionText(string text, string language)
+ {
+ AttributionText = new ToastGenericAttributionText()
+ {
+ Text = text
+ };
+
+ if (language != default)
+ {
+ AttributionText.Language = language;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Override the app logo with custom image of choice that will be displayed on the toast.
+ ///
+ /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ /// Specify how the image should be cropped.
+ /// A description of the image, for users of assistive technologies.
+ /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification.
+ /// The current instance of
+ public ToastContentBuilder AddAppLogoOverride(
+ Uri uri,
+ ToastGenericAppLogoCrop? hintCrop = default,
+ string alternateText = default,
+ bool? addImageQuery = default)
+ {
+ AppLogoOverrideUri = new ToastGenericAppLogo()
+ {
+ Source = uri.OriginalString
+ };
+
+ if (hintCrop != default)
+ {
+ AppLogoOverrideUri.HintCrop = hintCrop.Value;
+ }
+
+ if (alternateText != default)
+ {
+ AppLogoOverrideUri.AlternateText = alternateText;
+ }
+
+ if (addImageQuery != default)
+ {
+ AppLogoOverrideUri.AddImageQuery = addImageQuery;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Add a hero image to the toast.
+ ///
+ /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ /// A description of the image, for users of assistive technologies.
+ /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification.
+ /// The current instance of
+ public ToastContentBuilder AddHeroImage(
+ Uri uri,
+ string alternateText = default,
+ bool? addImageQuery = default)
+ {
+ HeroImage = new ToastGenericHeroImage()
+ {
+ Source = uri.OriginalString
+ };
+
+ if (alternateText != default)
+ {
+ HeroImage.AlternateText = alternateText;
+ }
+
+ if (addImageQuery != default)
+ {
+ HeroImage.AddImageQuery = addImageQuery;
+ }
+
+ return this;
+ }
+
+ ///
+ /// Add an image inline with other toast content.
+ ///
+ /// The URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ /// A description of the image, for users of assistive technologies.
+ /// A value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification.
+ /// A value whether a margin is removed. images have an 8px margin around them.
+ /// This property is not used. Setting this has no impact.
+ /// The current instance of
+ public ToastContentBuilder AddInlineImage(
+ Uri uri,
+ string alternateText = default,
+ bool? addImageQuery = default,
+ AdaptiveImageCrop? hintCrop = default,
+ bool? hintRemoveMargin = default)
+ {
+ var inlineImage = new AdaptiveImage()
+ {
+ Source = uri.OriginalString
+ };
+
+ if (hintCrop != null)
+ {
+ inlineImage.HintCrop = hintCrop.Value;
+ }
+
+ if (alternateText != default)
+ {
+ inlineImage.AlternateText = alternateText;
+ }
+
+ if (addImageQuery != default)
+ {
+ inlineImage.AddImageQuery = addImageQuery;
+ }
+
+ return AddVisualChild(inlineImage);
+ }
+
+ ///
+ /// Add a progress bar to the toast.
+ ///
+ /// Title of the progress bar.
+ /// Value of the progress bar. Default is 0
+ /// Determine if the progress bar value should be indeterminate. Default to false.
+ /// An optional string to be displayed instead of the default percentage string. If this isn't provided, something like "70%" will be displayed.
+ /// A status string which is displayed underneath the progress bar. This string should reflect the status of the operation, like "Downloading..." or "Installing...". Default to empty.
+ /// The current instance of
+ /// More info at: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/toast-progress-bar
+ public ToastContentBuilder AddProgressBar(string title = default, double? value = null, bool isIndeterminate = false, string valueStringOverride = default, string status = default)
+ {
+ int index = VisualChildren.Count(c => c is AdaptiveProgressBar);
+
+ var progressBar = new AdaptiveProgressBar()
+ {
+ };
+
+ if (title == default)
+ {
+ progressBar.Title = new BindableString($"progressBarTitle_{index}");
+ }
+ else
+ {
+ progressBar.Title = title;
+ }
+
+ if (isIndeterminate)
+ {
+ progressBar.Value = AdaptiveProgressBarValue.Indeterminate;
+ }
+ else if (value == null)
+ {
+ progressBar.Value = new BindableProgressBarValue($"progressValue_{index}");
+ }
+ else
+ {
+ progressBar.Value = value.Value;
+ }
+
+ if (valueStringOverride == default)
+ {
+ progressBar.ValueStringOverride = new BindableString($"progressValueString_{index}");
+ }
+ else
+ {
+ progressBar.ValueStringOverride = valueStringOverride;
+ }
+
+ if (status == default)
+ {
+ progressBar.Status = new BindableString($"progressStatus_{index}");
+ }
+ else
+ {
+ progressBar.Status = status;
+ }
+
+ return AddVisualChild(progressBar);
+ }
+
+ ///
+ /// Add text to the toast.
+ ///
+ /// Custom text to display on the tile.
+ /// This property is not used. Setting this has no effect.
+ /// This property is not used. Setting this has no effect. If you need to disable wrapping, set hintMaxLines to 1.
+ /// The maximum number of lines the text element is allowed to display.
+ /// hintMinLines is not used. Setting this has no effect.
+ /// hintAlign is not used. Setting this has no effect.
+ ///
+ /// The target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual.
+ ///
+ /// The current instance of
+ /// Throws when attempting to add/reserve more than 4 lines on a single toast.
+ /// Throws when value is larger than 2.
+ /// More info at: https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#text-elements
+ public ToastContentBuilder AddText(
+ string text,
+ AdaptiveTextStyle? hintStyle = null,
+ bool? hintWrap = default,
+ int? hintMaxLines = default,
+ int? hintMinLines = default,
+ AdaptiveTextAlign? hintAlign = null,
+ string language = default)
+ {
+ int lineCount = GetCurrentTextLineCount();
+ if (GetCurrentTextLineCount() == 4)
+ {
+ // Reached maximum, we can't go further.
+ throw new InvalidOperationException("We have reached max lines allowed (4) per toast");
+ }
+
+ AdaptiveText adaptive = new AdaptiveText()
+ {
+ Text = text
+ };
+
+ if (hintMaxLines != default)
+ {
+ if (hintMaxLines > 2)
+ {
+ throw new ArgumentOutOfRangeException(nameof(hintMaxLines), "max line can't go more than 2 lines.");
+ }
+ else if ((lineCount + hintMaxLines) > 4)
+ {
+ throw new InvalidOperationException($"Can't exceed more than 4 lines of text per toast. Current line count : {lineCount} | Requesting line count: {lineCount + hintMaxLines}");
+ }
+
+ adaptive.HintMaxLines = hintMaxLines;
+ }
+
+ if (language != default)
+ {
+ adaptive.Language = language;
+ }
+
+ return AddVisualChild(adaptive);
+ }
+
+ ///
+ /// Add a visual element to the toast.
+ ///
+ /// An instance of a class that implement .
+ /// The current instance of
+ public ToastContentBuilder AddVisualChild(IToastBindingGenericChild child)
+ {
+ VisualChildren.Add(child);
+
+ return this;
+ }
+
+ private int GetCurrentTextLineCount()
+ {
+ if (!VisualChildren.Any(c => c is AdaptiveText))
+ {
+ return 0;
+ }
+
+ var textList = VisualChildren.Where(c => c is AdaptiveText).Select(c => c as AdaptiveText).ToList();
+
+ // First one is already the header.
+ // https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#text-elements
+ // The default (and maximum) is up to 2 lines of text for the title, and up to 4 lines (combined) for the two additional description elements (the second and third AdaptiveText).
+ AdaptiveText text = textList.First();
+ int count = 0;
+ count += text.HintMaxLines ?? 2;
+
+ for (int i = 1; i < textList.Count; i++)
+ {
+ text = textList[i];
+ count += text.HintMaxLines ?? 1;
+ }
+
+ return count;
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/Builder/ToastContentBuilder.cs b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.cs
new file mode 100644
index 000000000..2be548c84
--- /dev/null
+++ b/components/Notifications/src/Toasts/Builder/ToastContentBuilder.cs
@@ -0,0 +1,439 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Builder class used to create
+ ///
+ public partial class ToastContentBuilder : IToastActivateableBuilder
+ {
+ private Dictionary _genericArguments = new Dictionary();
+
+ private bool _customArgumentsUsedOnToastItself;
+
+ ///
+ /// Gets internal instance of . This is equivalent to the call to .
+ ///
+ public ToastContent Content
+ {
+ get; private set;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ToastContentBuilder()
+ {
+ Content = new ToastContent();
+ }
+
+ ///
+ /// Add custom time stamp on the toast to override the time display on the toast.
+ ///
+ /// Custom Time to be displayed on the toast
+ /// The current instance of
+ public ToastContentBuilder AddCustomTimeStamp(DateTime dateTime)
+ {
+ Content.DisplayTimestamp = dateTime;
+
+ 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
+ public ToastContentBuilder AddHeader(string id, string title, ToastArguments arguments)
+ {
+ return AddHeader(id, title, arguments.ToString());
+ }
+
+ ///
+ /// 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.
+ /// A developer-defined string of arguments that is 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
+ public ToastContentBuilder AddHeader(string id, string title, string arguments)
+ {
+ Content.Header = new ToastHeader(id, title, arguments);
+
+ 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 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
+ public ToastContentBuilder 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
+ public ToastContentBuilder 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
+ 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
+ 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
+ public ToastContentBuilder AddArgument(string key, bool value)
+ {
+ return AddArgumentHelper(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space
+ }
+
+ ///
+ /// 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());
+ }
+
+ 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.CanAddArguments() && !b.ContainsArgument(key))
+ {
+ b.AddArgument(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 (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)
+ {
+ 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;
+ }
+
+ ///
+ /// Configures the toast notification to use background activation when the toast body is clicked.
+ ///
+ /// The current instance of
+ public ToastContentBuilder SetBackgroundActivation()
+ {
+ Content.ActivationType = ToastActivationType.Background;
+ return this;
+ }
+
+ ///
+ /// 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
+ /// The current instance of
+ public ToastContentBuilder AddToastActivationInfo(string launchArgs, ToastActivationType activationType)
+ {
+ Content.Launch = launchArgs;
+ Content.ActivationType = activationType;
+ _customArgumentsUsedOnToastItself = true;
+ return this;
+ }
+
+ ///
+ /// Sets the amount of time the Toast should display. You typically should use the
+ /// Scenario attribute instead, which impacts how long a Toast stays on screen.
+ ///
+ /// Duration of the toast
+ /// The current instance of
+ public ToastContentBuilder SetToastDuration(ToastDuration duration)
+ {
+ Content.Duration = duration;
+ return this;
+ }
+
+ ///
+ /// Sets the scenario, to make the Toast behave like an alarm, reminder, or more.
+ ///
+ /// Scenario to be used for the toast's behavior
+ /// The current instance of
+ public ToastContentBuilder SetToastScenario(ToastScenario scenario)
+ {
+ Content.Scenario = scenario;
+ return this;
+ }
+
+ ///
+ /// Set custom audio to go along with the toast.
+ ///
+ /// Source to the media that will be played when the toast is pop
+ /// Indicating whether sound should repeat as long as the Toast is shown; false to play only once (default).
+ /// Indicating whether sound is muted; false to allow the Toast notification sound to play (default).
+ /// The current instance of
+ public ToastContentBuilder AddAudio(
+ Uri src,
+ bool? loop = default,
+ bool? silent = default)
+ {
+ var audio = new ToastAudio();
+ audio.Src = src;
+
+ if (loop != default)
+ {
+ audio.Loop = loop.Value;
+ }
+
+ if (silent != default)
+ {
+ audio.Silent = silent.Value;
+ }
+
+ return AddAudio(audio);
+ }
+
+ ///
+ /// Set custom audio to go along with the toast.
+ ///
+ /// The to set.
+ /// The current instance of
+ public ToastContentBuilder AddAudio(ToastAudio audio)
+ {
+ if (audio.Src != null && !audio.Src.IsFile && audio.Src.Scheme != "ms-appx" && audio.Src.Scheme != "ms-winsoundevent")
+ {
+ throw new InvalidOperationException("Audio Source must either be a ms-appx file, absolute file, or ms-winsoundevent.");
+ }
+
+ Content.Audio = audio;
+ return this;
+ }
+
+ ///
+ /// Get the instance of that has been built by the builder with specified configuration so far.
+ ///
+ /// An instance of that can be used to create tile notification.
+ public ToastContent GetToastContent()
+ {
+ return Content;
+ }
+
+ ///
+ /// Retrieves the notification XML content as a WinRT XmlDocument, so that it can be used with a local Toast notification's constructor on either or .
+ ///
+ /// The notification XML content as a WinRT XmlDocument.
+ public Windows.Data.Xml.Dom.XmlDocument GetXml()
+ {
+ return GetToastContent().GetXml();
+ }
+
+ ///
+ /// Shows a new toast notification with the current content.
+ ///
+ public void Show()
+ {
+ CustomizeToast customize = null;
+ Show(customize);
+ }
+
+ ///
+ /// Shows a new toast notification with the current content.
+ ///
+ /// Allows you to set additional properties on the object.
+ public void Show(CustomizeToast customize)
+ {
+ var notif = new Windows.UI.Notifications.ToastNotification(GetToastContent().GetXml());
+ customize?.Invoke(notif);
+
+ ToastNotificationManagerCompat.CreateToastNotifier().Show(notif);
+ }
+
+ ///
+ /// Shows a new toast notification with the current content.
+ ///
+ /// Allows you to set additional properties on the object.
+ /// An operation that completes after your async customizations have completed.
+ public Windows.Foundation.IAsyncAction Show(CustomizeToastAsync customize)
+ {
+ return ShowAsyncHelper(customize).AsAsyncAction();
+ }
+
+ private async System.Threading.Tasks.Task ShowAsyncHelper(CustomizeToastAsync customize)
+ {
+ var notif = new Windows.UI.Notifications.ToastNotification(GetToastContent().GetXml());
+
+ if (customize != null)
+ {
+ await customize.Invoke(notif);
+ }
+
+ ToastNotificationManagerCompat.CreateToastNotifier().Show(notif);
+ }
+
+ ///
+ /// Schedules the notification.
+ ///
+ /// The date and time that Windows should display the toast notification. This time must be in the future.
+ public void Schedule(DateTimeOffset deliveryTime)
+ {
+ CustomizeScheduledToast customize = null;
+ Schedule(deliveryTime, customize);
+ }
+
+ ///
+ /// Schedules the notification.
+ ///
+ /// The date and time that Windows should display the toast notification. This time must be in the future.
+ /// Allows you to set additional properties on the object.
+ public void Schedule(DateTimeOffset deliveryTime, CustomizeScheduledToast customize)
+ {
+ var notif = new Windows.UI.Notifications.ScheduledToastNotification(GetToastContent().GetXml(), deliveryTime);
+ customize?.Invoke(notif);
+
+ ToastNotificationManagerCompat.CreateToastNotifier().AddToSchedule(notif);
+ }
+
+ ///
+ /// Schedules the notification.
+ ///
+ /// The date and time that Windows should display the toast notification. This time must be in the future.
+ /// Allows you to set additional properties on the object.
+ /// An operation that completes after your async customizations have completed.
+ public Windows.Foundation.IAsyncAction Schedule(DateTimeOffset deliveryTime, CustomizeScheduledToastAsync customize)
+ {
+ return ScheduleAsyncHelper(deliveryTime, customize).AsAsyncAction();
+ }
+
+ private async System.Threading.Tasks.Task ScheduleAsyncHelper(DateTimeOffset deliveryTime, CustomizeScheduledToastAsync customize = null)
+ {
+ var notif = new Windows.UI.Notifications.ScheduledToastNotification(GetToastContent().GetXml(), deliveryTime);
+
+ if (customize != null)
+ {
+ await customize.Invoke(notif);
+ }
+
+ ToastNotificationManagerCompat.CreateToastNotifier().AddToSchedule(notif);
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/Compat/ToastNotificationHistoryCompat.cs b/components/Notifications/src/Toasts/Compat/ToastNotificationHistoryCompat.cs
new file mode 100644
index 000000000..f94a46ff2
--- /dev/null
+++ b/components/Notifications/src/Toasts/Compat/ToastNotificationHistoryCompat.cs
@@ -0,0 +1,90 @@
+// 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.Collections.Generic;
+using Windows.UI.Notifications;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Manages the toast notifications for an app including the ability the clear all toast history and removing individual toasts.
+ ///
+ public class ToastNotificationHistoryCompat
+ {
+ private string _aumid;
+ private ToastNotificationHistory _history;
+
+ internal ToastNotificationHistoryCompat(string aumid)
+ {
+ _aumid = aumid;
+ _history = ToastNotificationManager.History;
+ }
+
+ ///
+ /// Removes all notifications sent by this app from action center.
+ ///
+ public void Clear()
+ {
+ if (_aumid != null)
+ {
+ _history.Clear(_aumid);
+ }
+ else
+ {
+ _history.Clear();
+ }
+ }
+
+ ///
+ /// Gets all notifications sent by this app that are currently still in Action Center.
+ ///
+ /// A collection of toasts.
+ public IReadOnlyList GetHistory()
+ {
+ return _aumid != null ? _history.GetHistory(_aumid) : _history.GetHistory();
+ }
+
+ ///
+ /// Removes an individual toast, with the specified tag label, from action center.
+ ///
+ /// The tag label of the toast notification to be removed.
+ public void Remove(string tag)
+ {
+ _history.Remove(tag);
+ }
+
+ ///
+ /// Removes a toast notification from the action using the notification's tag and group labels.
+ ///
+ /// The tag label of the toast notification to be removed.
+ /// The group label of the toast notification to be removed.
+ public void Remove(string tag, string group)
+ {
+ if (_aumid != null)
+ {
+ _history.Remove(tag, group, _aumid);
+ }
+ else
+ {
+ _history.Remove(tag, group);
+ }
+ }
+
+ ///
+ /// Removes a group of toast notifications, identified by the specified group label, from action center.
+ ///
+ /// The group label of the toast notifications to be removed.
+ public void RemoveGroup(string group)
+ {
+ if (_aumid != null)
+ {
+ _history.RemoveGroup(group, _aumid);
+ }
+ else
+ {
+ _history.RemoveGroup(group);
+ }
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/Compat/ToastNotificationManagerCompat.cs b/components/Notifications/src/Toasts/Compat/ToastNotificationManagerCompat.cs
new file mode 100644
index 000000000..443e359fb
--- /dev/null
+++ b/components/Notifications/src/Toasts/Compat/ToastNotificationManagerCompat.cs
@@ -0,0 +1,56 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using System.Reflection.Emit;
+using System.Runtime.InteropServices;
+using Microsoft.Win32;
+using Windows.ApplicationModel;
+using Windows.Foundation.Collections;
+using Windows.UI.Notifications;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Provides access to sending and managing toast notifications. Works for all types of apps, even Win32 non-MSIX/sparse apps.
+ ///
+ public static class ToastNotificationManagerCompat
+ {
+ ///
+ /// Creates a toast notifier.
+ ///
+ /// An instance of the toast notifier.
+ public static ToastNotifierCompat CreateToastNotifier()
+ {
+ return new ToastNotifierCompat(ToastNotificationManager.CreateToastNotifier());
+ }
+
+ ///
+ /// Gets the object.
+ ///
+ public static ToastNotificationHistoryCompat History
+ {
+ get
+ {
+ return new ToastNotificationHistoryCompat(null);
+ }
+ }
+
+ ///
+ /// Gets a value indicating whether http images can be used within toasts. This is true if running with package identity (UWP, MSIX, or sparse package).
+ ///
+ public static bool CanUseHttpImages
+ {
+ get
+ {
+ return true;
+ }
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/Compat/ToastNotifierCompat.cs b/components/Notifications/src/Toasts/Compat/ToastNotifierCompat.cs
new file mode 100644
index 000000000..a27ff19a5
--- /dev/null
+++ b/components/Notifications/src/Toasts/Compat/ToastNotifierCompat.cs
@@ -0,0 +1,101 @@
+// 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.Collections.Generic;
+using Windows.UI.Notifications;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Allows you to show and schedule toast notifications.
+ ///
+ public sealed class ToastNotifierCompat
+ {
+ private ToastNotifier _notifier;
+
+ internal ToastNotifierCompat(ToastNotifier notifier)
+ {
+ _notifier = notifier;
+ }
+
+ ///
+ /// Displays the specified toast notification.
+ ///
+ /// The object that contains the content of the toast notification to display.
+ public void Show(ToastNotification notification)
+ {
+ _notifier.Show(notification);
+ }
+
+ ///
+ /// Hides the specified toast notification from the screen (moves it into Action Center).
+ ///
+ /// The object that specifies the toast to hide.
+ public void Hide(ToastNotification notification)
+ {
+ _notifier.Hide(notification);
+ }
+
+ ///
+ /// Adds a ScheduledToastNotification for later display by Windows.
+ ///
+ /// The scheduled toast notification, which includes its content and timing instructions.
+ public void AddToSchedule(ScheduledToastNotification scheduledToast)
+ {
+ _notifier.AddToSchedule(scheduledToast);
+ }
+
+ ///
+ /// Cancels the scheduled display of a specified ScheduledToastNotification.
+ ///
+ /// The notification to remove from the schedule.
+ public void RemoveFromSchedule(ScheduledToastNotification scheduledToast)
+ {
+ _notifier.RemoveFromSchedule(scheduledToast);
+ }
+
+ ///
+ /// Gets the collection of ScheduledToastNotification objects that this app has scheduled for display.
+ ///
+ /// The collection of scheduled toast notifications that the app bound to this notifier has scheduled for timed display.
+ public IReadOnlyList GetScheduledToastNotifications()
+ {
+ return _notifier.GetScheduledToastNotifications();
+ }
+
+ ///
+ /// Updates the existing toast notification that has the specified tag and belongs to the specified notification group.
+ ///
+ /// An object that contains the updated info.
+ /// The identifier of the toast notification to update.
+ /// The ID of the ToastCollection that contains the notification.
+ /// A value that indicates the result of the update (failure, success, etc).
+ public NotificationUpdateResult Update(NotificationData data, string tag, string group)
+ {
+ return _notifier.Update(data, tag, group);
+ }
+
+ ///
+ /// Updates the existing toast notification that has the specified tag.
+ ///
+ /// An object that contains the updated info.
+ /// The identifier of the toast notification to update.
+ /// A value that indicates the result of the update (failure, success, etc).
+ public NotificationUpdateResult Update(NotificationData data, string tag)
+ {
+ return _notifier.Update(data, tag);
+ }
+
+ ///
+ /// Gets a value that tells you whether there is an app, user, or system block that prevents the display of a toast notification.
+ ///
+ public NotificationSetting Setting
+ {
+ get
+ {
+ return _notifier.Setting;
+ }
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/Elements/Element_Toast.cs b/components/Notifications/src/Toasts/Elements/Element_Toast.cs
new file mode 100644
index 000000000..1e158248c
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_Toast.cs
@@ -0,0 +1,156 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_Toast : BaseElement, IElement_ToastActivatable, IHaveXmlAdditionalProperties, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ internal const ToastScenario DEFAULT_SCENARIO = ToastScenario.Default;
+ internal const Element_ToastActivationType DEFAULT_ACTIVATION_TYPE = Element_ToastActivationType.Foreground;
+ internal const ToastDuration DEFAULT_DURATION = ToastDuration.Short;
+
+ public Element_ToastActivationType ActivationType { get; set; } = DEFAULT_ACTIVATION_TYPE;
+
+ public string ProtocolActivationTargetApplicationPfn { get; set; }
+
+ public ToastAfterActivationBehavior AfterActivationBehavior
+ {
+ get
+ {
+ return ToastAfterActivationBehavior.Default;
+ }
+
+ set
+ {
+ if (value != ToastAfterActivationBehavior.Default)
+ {
+ throw new InvalidOperationException("AfterActivationBehavior on ToastContent only supports the Default value.");
+ }
+ }
+ }
+
+ public ToastDuration Duration { get; set; } = DEFAULT_DURATION;
+
+ public string Launch { get; set; }
+
+ public ToastScenario Scenario { get; set; } = DEFAULT_SCENARIO;
+
+ public DateTimeOffset? DisplayTimestamp { get; set; }
+
+ public Element_ToastVisual Visual { get; set; }
+
+ public Element_ToastAudio Audio { get; set; }
+
+ public Element_ToastActions Actions { get; set; }
+
+ public Element_ToastHeader Header { get; set; }
+
+ public string HintToastId { get; set; }
+
+ public string HintPeople { get; set; }
+
+ public IReadOnlyDictionary AdditionalProperties { get; set; }
+
+ public static Element_ToastActivationType ConvertActivationType(ToastActivationType publicType)
+ {
+ switch (publicType)
+ {
+ case ToastActivationType.Foreground:
+ return Element_ToastActivationType.Foreground;
+
+ case ToastActivationType.Background:
+ return Element_ToastActivationType.Background;
+
+ case ToastActivationType.Protocol:
+ return Element_ToastActivationType.Protocol;
+
+ default:
+ throw new NotImplementedException();
+ }
+ }
+
+ ///
+ string IHaveXmlName.Name => "toast";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => new object[] { Visual, Audio, Actions, Header };
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ if (ActivationType != DEFAULT_ACTIVATION_TYPE)
+ {
+ yield return new("activationType", ActivationType.ToPascalCaseString());
+ }
+
+ yield return new("protocolActivationTargetApplicationPfn", ProtocolActivationTargetApplicationPfn);
+
+ if (AfterActivationBehavior != ToastAfterActivationBehavior.Default)
+ {
+ yield return new("afterActivationBehavior", AfterActivationBehavior.ToPascalCaseString());
+ }
+
+ if (Duration != DEFAULT_DURATION)
+ {
+ yield return new("duration", Duration.ToPascalCaseString());
+ }
+
+ yield return new("launch", Launch);
+
+ if (Scenario != DEFAULT_SCENARIO)
+ {
+ yield return new("scenario", Scenario.ToPascalCaseString());
+ }
+
+ yield return new("displayTimestamp", DisplayTimestamp);
+ yield return new("hint-toastId", HintToastId);
+ yield return new("hint-people", HintPeople);
+ }
+ }
+
+ ///
+ /// The amount of time the Toast should display.
+ ///
+ public enum ToastDuration
+ {
+ ///
+ /// Default value. Toast appears for a short while and then goes into Action Center.
+ ///
+ Short,
+
+ ///
+ /// Toast stays on-screen for longer, and then goes into Action Center.
+ ///
+ Long
+ }
+
+ ///
+ /// Specifies the scenario, controlling behaviors about the Toast.
+ ///
+ public enum ToastScenario
+ {
+ ///
+ /// The normal Toast behavior. The Toast appears for a short duration, and then automatically dismisses into Action Center.
+ ///
+ Default,
+
+ ///
+ /// Causes the Toast to stay on-screen and expanded until the user takes action. Also causes a looping alarm sound to be selected by default.
+ ///
+ Alarm,
+
+ ///
+ /// Causes the Toast to stay on-screen and expanded until the user takes action.
+ ///
+ Reminder,
+
+ ///
+ /// Causes the Toast to stay on-screen and expanded until the user takes action (on Mobile this expands to full screen). Also causes a looping incoming call sound to be selected by default.
+ ///
+ IncomingCall
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastAction.cs b/components/Notifications/src/Toasts/Elements/Element_ToastAction.cs
new file mode 100644
index 000000000..eaab27b19
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastAction.cs
@@ -0,0 +1,106 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastAction : IElement_ToastActionsChild, IElement_ToastActivatable, IHaveXmlName, IHaveXmlNamedProperties
+ {
+ internal const Element_ToastActivationType DEFAULT_ACTIVATION_TYPE = Element_ToastActivationType.Foreground;
+ internal const ToastAfterActivationBehavior DEFAULT_AFTER_ACTIVATION_BEHAVIOR = ToastAfterActivationBehavior.Default;
+ internal const Element_ToastActionPlacement DEFAULT_PLACEMENT = Element_ToastActionPlacement.Inline;
+
+ ///
+ /// Gets or sets the text to be displayed on the button.
+ ///
+ public string Content { get; set; }
+
+ ///
+ /// Gets or sets the arguments attribute describing the app-defined data that the app can later retrieve once it is activated from user taking this action.
+ ///
+ public string Arguments { get; set; }
+
+ public Element_ToastActivationType ActivationType { get; set; } = DEFAULT_ACTIVATION_TYPE;
+
+ public string ProtocolActivationTargetApplicationPfn { get; set; }
+
+ public ToastAfterActivationBehavior AfterActivationBehavior { get; set; } = DEFAULT_AFTER_ACTIVATION_BEHAVIOR;
+
+ ///
+ /// Gets or sets optional value to provide an image icon for this action to display inside the button alone with the text content.
+ ///
+ public string ImageUri { get; set; }
+
+ ///
+ /// Gets or sets value used for the quick reply scenario.
+ ///
+ public string InputId { get; set; }
+
+ public Element_ToastActionPlacement Placement { get; set; } = DEFAULT_PLACEMENT;
+
+ public string HintActionId { get; set; }
+
+ ///
+ string IHaveXmlName.Name => "action";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("content", Content);
+ yield return new("arguments", Arguments);
+
+ if (ActivationType != DEFAULT_ACTIVATION_TYPE)
+ {
+ yield return new("activationType", ActivationType.ToPascalCaseString());
+ }
+
+ yield return new("protocolActivationTargetApplicationPfn", ProtocolActivationTargetApplicationPfn);
+
+ if (AfterActivationBehavior != DEFAULT_AFTER_ACTIVATION_BEHAVIOR)
+ {
+ yield return new("afterActivationBehavior", AfterActivationBehavior.ToPascalCaseString());
+ }
+
+ yield return new("imageUri", ImageUri);
+ yield return new("hint-inputId", InputId);
+
+ if (Placement != DEFAULT_PLACEMENT)
+ {
+ yield return new("placement", Placement.ToPascalCaseString());
+ }
+
+ yield return new("hint-actionId", HintActionId);
+ }
+ }
+
+ internal enum Element_ToastActionPlacement
+ {
+ Inline,
+ ContextMenu
+ }
+
+ internal enum Element_ToastActivationType
+ {
+ ///
+ /// Default value. Your foreground app is launched.
+ ///
+ Foreground,
+
+ ///
+ /// Your corresponding background task (assuming you set everything up) is triggered, and you can execute code in the background (like sending the user's quick reply message) without interrupting the user.
+ ///
+ Background,
+
+ ///
+ /// Launch a different app using protocol activation.
+ ///
+ Protocol,
+
+ ///
+ /// System handles the activation.
+ ///
+ System
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastActions.cs b/components/Notifications/src/Toasts/Elements/Element_ToastActions.cs
new file mode 100644
index 000000000..c5ff90347
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastActions.cs
@@ -0,0 +1,42 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastActions : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ internal const ToastSystemCommand DEFAULT_SYSTEM_COMMAND = ToastSystemCommand.None;
+
+ public ToastSystemCommand SystemCommands { get; set; } = ToastSystemCommand.None;
+
+ public IList Children { get; private set; } = new List();
+
+ ///
+ string IHaveXmlName.Name => "actions";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Children;
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ if (SystemCommands != DEFAULT_SYSTEM_COMMAND)
+ {
+ yield return new("hint-systemCommands", SystemCommands);
+ }
+ }
+ }
+
+ internal interface IElement_ToastActionsChild
+ {
+ }
+
+ internal enum ToastSystemCommand
+ {
+ None,
+ SnoozeAndDismiss
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastAudio.cs b/components/Notifications/src/Toasts/Elements/Element_ToastAudio.cs
new file mode 100644
index 000000000..2a674556a
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastAudio.cs
@@ -0,0 +1,46 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastAudio : IHaveXmlName, IHaveXmlNamedProperties
+ {
+ internal const bool DEFAULT_LOOP = false;
+ internal const bool DEFAULT_SILENT = false;
+
+ ///
+ /// Gets or sets the media file to play in place of the default sound. This can either be a ms-winsoundevent value, or a custom ms-appx:/// or ms-appdata:/// file, or null for the default sound.
+ ///
+ public Uri Src { get; set; }
+
+ public bool Loop { get; set; } = DEFAULT_LOOP;
+
+ ///
+ /// Gets or sets a value indicating whether the sound is muted; false to allow the Toast notification sound to play.
+ ///
+ public bool Silent { get; set; } = DEFAULT_SILENT;
+
+ ///
+ string IHaveXmlName.Name => "audio";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("src", Src);
+
+ if (Loop != DEFAULT_LOOP)
+ {
+ yield return new("loop", Loop);
+ }
+
+ if (Silent != DEFAULT_SILENT)
+ {
+ yield return new("silent", Silent);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastBinding.cs b/components/Notifications/src/Toasts/Elements/Element_ToastBinding.cs
new file mode 100644
index 000000000..c5a680a74
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastBinding.cs
@@ -0,0 +1,77 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastBinding : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ public Element_ToastBinding(ToastTemplateType template)
+ {
+ Template = template;
+ }
+
+ public ToastTemplateType Template { get; private set; }
+
+ ///
+ /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language; for instance, a value of
+ ///
+ /// "www.website.com/images/hello.png"
+ ///
+ /// included in the notification becomes
+ ///
+ /// "www.website.com/images/hello.png?ms-scale=100&ms-contrast=standard&ms-lang=en-us"
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ ///
+ /// Gets or sets a default base URI that is combined with relative URIs in image source attributes.
+ ///
+ public Uri BaseUri { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides that in visual, but can be overridden by that in text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string. See Remarks for when this value isn't specified.
+ ///
+ public string Language { get; set; }
+
+ public string ExperienceType { get; set; }
+
+ public IList Children { get; private set; } = new List();
+
+ ///
+ string IHaveXmlName.Name => "binding";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Children;
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("template", Template);
+ yield return new("addImageQuery", AddImageQuery);
+ yield return new("baseUri", BaseUri);
+ yield return new("lang", Language);
+ yield return new("experienceType", ExperienceType);
+ }
+ }
+
+ internal interface IElement_ToastBindingChild
+ {
+ }
+
+ internal enum ToastTemplateType
+ {
+ ToastGeneric,
+ ToastImageAndText01,
+ ToastImageAndText02,
+ ToastImageAndText03,
+ ToastImageAndText04,
+ ToastText01,
+ ToastText02,
+ ToastText03,
+ ToastText04
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastHeader.cs b/components/Notifications/src/Toasts/Elements/Element_ToastHeader.cs
new file mode 100644
index 000000000..7b1f5aec7
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastHeader.cs
@@ -0,0 +1,61 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastHeader : IElement_ToastActivatable, IHaveXmlName, IHaveXmlNamedProperties
+ {
+ public string Id { get; set; }
+
+ public string Title { get; set; }
+
+ public string Arguments { get; set; }
+
+ public Element_ToastActivationType ActivationType { get; set; } = Element_ToastActivationType.Foreground;
+
+ public string ProtocolActivationTargetApplicationPfn { get; set; }
+
+ public ToastAfterActivationBehavior AfterActivationBehavior
+ {
+ get
+ {
+ return ToastAfterActivationBehavior.Default;
+ }
+
+ set
+ {
+ if (value != ToastAfterActivationBehavior.Default)
+ {
+ throw new InvalidOperationException("AfterActivationBehavior on ToastHeader only supports the Default value.");
+ }
+ }
+ }
+
+ ///
+ string IHaveXmlName.Name => "header";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("id", Id);
+ yield return new("title", Title);
+ yield return new("arguments", Arguments);
+
+ if (ActivationType != Element_ToastActivationType.Foreground)
+ {
+ yield return new("activationType", ActivationType.ToPascalCaseString());
+ }
+
+ yield return new("protocolActivationTargetApplicationPfn", ProtocolActivationTargetApplicationPfn);
+
+ if (AfterActivationBehavior != ToastAfterActivationBehavior.Default)
+ {
+ yield return new("afterActivationBehavior", AfterActivationBehavior.ToPascalCaseString());
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastImage.cs b/components/Notifications/src/Toasts/Elements/Element_ToastImage.cs
new file mode 100644
index 000000000..e3473a278
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastImage.cs
@@ -0,0 +1,73 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastImage : IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties
+ {
+ internal const ToastImagePlacement DEFAULT_PLACEMENT = ToastImagePlacement.Inline;
+ internal const bool DEFAULT_ADD_IMAGE_QUERY = false;
+ internal const ToastImageCrop DEFAULT_CROP = ToastImageCrop.None;
+
+ public string Src { get; set; }
+
+ public string Alt { get; set; }
+
+ public bool AddImageQuery { get; set; } = DEFAULT_ADD_IMAGE_QUERY;
+
+ public ToastImagePlacement Placement { get; set; } = DEFAULT_PLACEMENT;
+
+ public ToastImageCrop Crop { get; set; } = DEFAULT_CROP;
+
+ ///
+ string IHaveXmlName.Name => "image";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("src", Src);
+ yield return new("alt", Alt);
+
+ if (AddImageQuery != DEFAULT_ADD_IMAGE_QUERY)
+ {
+ yield return new("addImageQuery", AddImageQuery);
+ }
+
+ if (Placement != DEFAULT_PLACEMENT)
+ {
+ yield return new("placement", Placement.ToPascalCaseString());
+ }
+
+ if (Crop != DEFAULT_CROP)
+ {
+ yield return new("crop", Crop.ToPascalCaseString());
+ }
+ }
+ }
+
+ ///
+ /// Specify the desired cropping of the image.
+ ///
+ public enum ToastImageCrop
+ {
+ ///
+ /// Default value. Image is not cropped.
+ ///
+ None,
+
+ ///
+ /// Image is cropped to a circle shape.
+ ///
+ Circle
+ }
+
+ internal enum ToastImagePlacement
+ {
+ Inline,
+ AppLogoOverride,
+ Hero
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastInput.cs b/components/Notifications/src/Toasts/Elements/Element_ToastInput.cs
new file mode 100644
index 000000000..0e8cd882b
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastInput.cs
@@ -0,0 +1,61 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastInput : IElement_ToastActionsChild, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ ///
+ /// Gets or sets the required attributes for developers to retrieve user inputs once the app is activated (in the foreground or background).
+ ///
+ public string Id { get; set; }
+
+ public ToastInputType Type { get; set; }
+
+ ///
+ /// Gets or sets the optional title attribute and is for developers to specify a title for the input for shells to render when there is affordance.
+ ///
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets the optional placeholderContent attribute and is the grey-out hint text for text input type. This attribute is ignored when the input type is not �text�.
+ ///
+ public string PlaceholderContent { get; set; }
+
+ ///
+ /// Gets or sets the optional defaultInput attribute and it allows developer to provide a default input value.
+ ///
+ public string DefaultInput { get; set; }
+
+ public IList Children { get; private set; } = new List();
+
+ ///
+ string IHaveXmlName.Name => "input";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Children;
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("id", Id);
+ yield return new("type", Type.ToPascalCaseString());
+ yield return new("title", Title);
+ yield return new("placeHolderContent", PlaceholderContent);
+ yield return new("defaultInput", DefaultInput);
+ }
+ }
+
+ internal interface IElement_ToastInputChild
+ {
+ }
+
+ internal enum ToastInputType
+ {
+ Text,
+ Selection
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastSelection.cs b/components/Notifications/src/Toasts/Elements/Element_ToastSelection.cs
new file mode 100644
index 000000000..4092ad554
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastSelection.cs
@@ -0,0 +1,31 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastSelection : IElement_ToastInputChild, IHaveXmlName, IHaveXmlNamedProperties
+ {
+ ///
+ /// Gets or sets the id attribute for apps to retrieve back the user selected input after the app is activated. Required
+ ///
+ public string Id { get; set; }
+
+ ///
+ /// Gets or sets the text to display for this selection element.
+ ///
+ public string Content { get; set; }
+
+ ///
+ string IHaveXmlName.Name => "selection";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("id", Id);
+ yield return new("content", Content);
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastText.cs b/components/Notifications/src/Toasts/Elements/Element_ToastText.cs
new file mode 100644
index 000000000..f9b9947aa
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastText.cs
@@ -0,0 +1,39 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastText : IElement_ToastBindingChild, IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlText
+ {
+ internal const ToastTextPlacement DEFAULT_PLACEMENT = ToastTextPlacement.Inline;
+
+ public string Text { get; set; }
+
+ public string Lang { get; set; }
+
+ public ToastTextPlacement Placement { get; set; } = DEFAULT_PLACEMENT;
+
+ ///
+ string IHaveXmlName.Name => "text";
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("lang", Lang);
+
+ if (Placement != DEFAULT_PLACEMENT)
+ {
+ yield return new("placement", Placement.ToPascalCaseString());
+ }
+ }
+ }
+
+ internal enum ToastTextPlacement
+ {
+ Inline,
+ Attribution
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/Element_ToastVisual.cs b/components/Notifications/src/Toasts/Elements/Element_ToastVisual.cs
new file mode 100644
index 000000000..0baf45b1a
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/Element_ToastVisual.cs
@@ -0,0 +1,39 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ internal sealed class Element_ToastVisual : IHaveXmlName, IHaveXmlNamedProperties, IHaveXmlChildren
+ {
+ internal const bool DEFAULT_ADD_IMAGE_QUERY = false;
+
+ public bool? AddImageQuery { get; set; }
+
+ public Uri BaseUri { get; set; }
+
+ public string Language { get; set; }
+
+ public int? Version { get; set; }
+
+ public IList Bindings { get; private set; } = new List();
+
+ ///
+ string IHaveXmlName.Name => "visual";
+
+ ///
+ IEnumerable IHaveXmlChildren.Children => Bindings;
+
+ ///
+ IEnumerable> IHaveXmlNamedProperties.EnumerateNamedProperties()
+ {
+ yield return new("addImageQuery", AddImageQuery);
+ yield return new("baseUri", BaseUri);
+ yield return new("lang", Language);
+ yield return new("version", Version);
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/Elements/IElement_ToastActivatable.cs b/components/Notifications/src/Toasts/Elements/IElement_ToastActivatable.cs
new file mode 100644
index 000000000..bc4a7d293
--- /dev/null
+++ b/components/Notifications/src/Toasts/Elements/IElement_ToastActivatable.cs
@@ -0,0 +1,15 @@
+// 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 CommunityToolkit.Notifications
+{
+ internal interface IElement_ToastActivatable
+ {
+ Element_ToastActivationType ActivationType { get; set; }
+
+ string ProtocolActivationTargetApplicationPfn { get; set; }
+
+ ToastAfterActivationBehavior AfterActivationBehavior { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/IToastActions.cs b/components/Notifications/src/Toasts/IToastActions.cs
new file mode 100644
index 000000000..4b7627307
--- /dev/null
+++ b/components/Notifications/src/Toasts/IToastActions.cs
@@ -0,0 +1,19 @@
+// 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.Collections.Generic;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Actions to display on a Toast notification. One of or .
+ ///
+ public interface IToastActions
+ {
+ ///
+ /// Gets custom context menu items, providing additional actions when the user right clicks the Toast notification. New in Anniversary Update
+ ///
+ IList ContextMenuItems { get; }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/IToastActivateableBuilder.cs b/components/Notifications/src/Toasts/IToastActivateableBuilder.cs
new file mode 100644
index 000000000..319575498
--- /dev/null
+++ b/components/Notifications/src/Toasts/IToastActivateableBuilder.cs
@@ -0,0 +1,91 @@
+// 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 CommunityToolkit.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.
+ 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);
+
+ ///
+ /// 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);
+
+ ///
+ /// 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/components/Notifications/src/Toasts/IToastBindingGenericChild.cs b/components/Notifications/src/Toasts/IToastBindingGenericChild.cs
new file mode 100644
index 000000000..444cefbe6
--- /dev/null
+++ b/components/Notifications/src/Toasts/IToastBindingGenericChild.cs
@@ -0,0 +1,14 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Elements that can be direct children of , including ( , , and ).
+ ///
+ public interface IToastBindingGenericChild
+ {
+ // Blank interface simply for compile-enforcing the child types in the list.
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/IToastButton.cs b/components/Notifications/src/Toasts/IToastButton.cs
new file mode 100644
index 000000000..0f52fee31
--- /dev/null
+++ b/components/Notifications/src/Toasts/IToastButton.cs
@@ -0,0 +1,24 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// One of , , or .
+ ///
+ public interface IToastButton
+ {
+ ///
+ /// Gets or sets an optional image icon for the button to display (required for buttons adjacent to inputs like quick reply).
+ ///
+ string ImageUri { get; set; }
+
+ ///
+ /// Gets or 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.
+ ///
+ string HintActionId { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/IToastInput.cs b/components/Notifications/src/Toasts/IToastInput.cs
new file mode 100644
index 000000000..7b13e568d
--- /dev/null
+++ b/components/Notifications/src/Toasts/IToastInput.cs
@@ -0,0 +1,13 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// An input element on a Toast notification. One of or .
+ ///
+ public interface IToastInput
+ {
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastActionsCustom.cs b/components/Notifications/src/Toasts/ToastActionsCustom.cs
new file mode 100644
index 000000000..a40f8d702
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastActionsCustom.cs
@@ -0,0 +1,94 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Create your own custom actions, using controls like , , and .
+ ///
+ public sealed class ToastActionsCustom : IToastActions
+ {
+ ///
+ /// Gets inputs like and . Only up to 5 inputs can be added; after that, an exception is thrown.
+ ///
+ public IList Inputs { get; private set; } = new LimitedList(5);
+
+ ///
+ /// Gets buttons displayed after all the inputs (or adjacent to inputs if used as quick reply buttons). Only up to 5 buttons can be added (or fewer if you are also including context menu items). After that, an exception is thrown. You can add , , or
+ ///
+ public IList Buttons { get; private set; } = new LimitedList(5);
+
+ ///
+ /// Gets custom context menu items, providing additional actions when the user right clicks the Toast notification.
+ /// You can only have up to 5 buttons and context menu items *combined*. Thus, if you have one context menu item,
+ /// you can only have four buttons, etc. New in Anniversary Update:
+ ///
+ public IList ContextMenuItems { get; private set; } = new List();
+
+ internal Element_ToastActions ConvertToElement()
+ {
+ if (Buttons.Count + ContextMenuItems.Count > 5)
+ {
+ throw new InvalidOperationException("You have too many buttons/context menu items. You can only have up to 5 total.");
+ }
+
+ var el = new Element_ToastActions();
+
+ foreach (var input in Inputs)
+ {
+ el.Children.Add(ConvertToInputElement(input));
+ }
+
+ foreach (var button in this.Buttons)
+ {
+ el.Children.Add(ConvertToActionElement(button));
+ }
+
+ foreach (var item in ContextMenuItems)
+ {
+ el.Children.Add(item.ConvertToElement());
+ }
+
+ return el;
+ }
+
+ private static Element_ToastAction ConvertToActionElement(IToastButton button)
+ {
+ if (button is ToastButton)
+ {
+ return (button as ToastButton).ConvertToElement();
+ }
+
+ if (button is ToastButtonDismiss)
+ {
+ return (button as ToastButtonDismiss).ConvertToElement();
+ }
+
+ if (button is ToastButtonSnooze)
+ {
+ return (button as ToastButtonSnooze).ConvertToElement();
+ }
+
+ throw new NotImplementedException("Unknown button child: " + button.GetType());
+ }
+
+ private static Element_ToastInput ConvertToInputElement(IToastInput input)
+ {
+ if (input is ToastTextBox)
+ {
+ return (input as ToastTextBox).ConvertToElement();
+ }
+
+ if (input is ToastSelectionBox)
+ {
+ return (input as ToastSelectionBox).ConvertToElement();
+ }
+
+ throw new NotImplementedException("Unknown input child: " + input.GetType());
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastActionsSnoozeAndDismiss.cs b/components/Notifications/src/Toasts/ToastActionsSnoozeAndDismiss.cs
new file mode 100644
index 000000000..13d5a76ff
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastActionsSnoozeAndDismiss.cs
@@ -0,0 +1,41 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Automatically constructs a selection box for snooze intervals, and snooze/dismiss buttons, all automatically localized, and snoozing logic is automatically handled by the system.
+ ///
+ public sealed class ToastActionsSnoozeAndDismiss : IToastActions
+ {
+ ///
+ /// Gets custom context menu items, providing additional actions when the user right clicks the Toast notification.
+ /// You can only have up to 5 items. New in Anniversary Update
+ ///
+ public IList ContextMenuItems { get; private set; } = new List();
+
+ internal Element_ToastActions ConvertToElement()
+ {
+ if (ContextMenuItems.Count > 5)
+ {
+ throw new InvalidOperationException("You have too many context menu items. You can only have up to 5.");
+ }
+
+ var el = new Element_ToastActions()
+ {
+ SystemCommands = ToastSystemCommand.SnoozeAndDismiss
+ };
+
+ foreach (var item in ContextMenuItems)
+ {
+ el.Children.Add(item.ConvertToElement());
+ }
+
+ return el;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastActivationOptions.cs b/components/Notifications/src/Toasts/ToastActivationOptions.cs
new file mode 100644
index 000000000..81609903a
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastActivationOptions.cs
@@ -0,0 +1,38 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// New in Creators Update: Additional options relating to activation.
+ ///
+ public sealed class ToastActivationOptions
+ {
+ ///
+ /// Gets or sets the target PFN if you are using . You can optionally specify, so that regardless of whether multiple apps are registered to handle the same protocol uri, your desired app will always be launched.
+ ///
+ public string ProtocolActivationTargetApplicationPfn { get; set; }
+
+ ///
+ /// Gets or sets the behavior that the toast should use when the user invokes this action.
+ /// Note that this option only works on and .
+ /// Desktop-only, supported in builds 16251 or higher. New in Fall Creators Update
+ ///
+ public ToastAfterActivationBehavior AfterActivationBehavior { get; set; } = ToastAfterActivationBehavior.Default;
+
+ internal void PopulateElement(IElement_ToastActivatable el)
+ {
+ // If protocol PFN is specified but protocol activation isn't used, throw exception
+ if (ProtocolActivationTargetApplicationPfn != null && el.ActivationType != Element_ToastActivationType.Protocol)
+ {
+ throw new InvalidOperationException($"You cannot specify {nameof(ProtocolActivationTargetApplicationPfn)} without using ActivationType of Protocol.");
+ }
+
+ el.ProtocolActivationTargetApplicationPfn = ProtocolActivationTargetApplicationPfn;
+ el.AfterActivationBehavior = AfterActivationBehavior;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastArguments.cs b/components/Notifications/src/Toasts/ToastArguments.cs
new file mode 100644
index 000000000..de9862c11
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastArguments.cs
@@ -0,0 +1,429 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// 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>
+ {
+ private Dictionary _dictionary = new Dictionary();
+
+ internal ToastArguments Clone()
+ {
+ return new ToastArguments()
+ {
+ _dictionary = new Dictionary(_dictionary)
+ };
+ }
+
+ ///
+ /// Gets the value of the specified key. Throws if the key could not be found.
+ ///
+ /// The key to find.
+ /// The value of the specified key.
+ public string this[string key]
+ {
+ get
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (TryGetValue(key, out string value))
+ {
+ return value;
+ }
+
+ throw new KeyNotFoundException($"A key with name '{key}' could not be found.");
+ }
+
+ set
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ _dictionary[key] = value;
+ }
+ }
+
+ ///
+ /// Attempts to get the value of the specified key. If no key exists, returns false.
+ ///
+ /// 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.
+ public bool TryGetValue(string key, out string value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return _dictionary.TryGetValue(key, out value);
+ }
+
+ ///
+ /// Attempts to get the value of the specified key. If no key exists, returns false.
+ ///
+ /// The enum to parse.
+ /// 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.
+ public bool TryGetValue(string key, out T value)
+ where T : struct, Enum
+ {
+ if (TryGetValue(key, out string strValue))
+ {
+ return Enum.TryParse(strValue, out value);
+ }
+
+ value = default(T);
+ return false;
+ }
+
+ ///
+ /// Gets the value of the specified key, or throws if key didn't exist.
+ ///
+ /// The key to get.
+ /// The value of the key.
+ public string Get(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ if (_dictionary.TryGetValue(key, out string value))
+ {
+ return value;
+ }
+
+ throw new KeyNotFoundException();
+ }
+
+ ///
+ /// Gets the value of the specified key, or throws if key didn't exist.
+ ///
+ /// The key to get.
+ /// The value of the key.
+ public int GetInt(string key)
+ {
+ return int.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 double GetDouble(string key)
+ {
+ return double.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 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.
+ ///
+ /// The key to get.
+ /// The value of the key.
+ public bool GetBool(string key)
+ {
+ return Get(key) == "1" ? true : false;
+ }
+
+ ///
+ /// Gets the value of the specified key, or throws if key didn't exist.
+ ///
+ /// The enum to parse.
+ /// The key to get.
+ /// The value of the key.
+ public T GetEnum(string key)
+ where T : struct, Enum
+ {
+ if (TryGetValue(key, out T value))
+ {
+ return value;
+ }
+
+ throw new KeyNotFoundException();
+ }
+
+ ///
+ /// Gets the number of key/value pairs contained in the toast arguments.
+ ///
+ public int Count => _dictionary.Count;
+
+ ///
+ /// Adds a key. If there is an existing key, it is replaced.
+ ///
+ /// The key.
+ /// The current object.
+ public ToastArguments Add(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ _dictionary[key] = null;
+
+ return this;
+ }
+
+ ///
+ /// Adds a key and optional value. If there is an existing key, it is replaced.
+ ///
+ /// The key.
+ /// The optional value of the key.
+ /// The current object.
+ public ToastArguments Add(string key, string value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ _dictionary[key] = value;
+
+ return this;
+ }
+
+ ///
+ /// Adds 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 Add(string key, int value)
+ {
+ return AddHelper(key, value);
+ }
+
+ ///
+ /// Adds 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 Add(string key, double value)
+ {
+ return AddHelper(key, value);
+ }
+
+ ///
+ /// Adds 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 Add(string key, float value)
+ {
+ return AddHelper(key, value);
+ }
+
+ ///
+ /// Adds 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 Add(string key, bool value)
+ {
+ return Add(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space
+ }
+
+ ///
+ /// 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 Add(string key, Enum value)
+ {
+ return Add(key, (int)(object)value);
+ }
+
+ private ToastArguments AddHelper(string key, object value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ _dictionary[key] = value.ToString();
+
+ return this;
+ }
+
+ ///
+ /// Determines if the specified key is present.
+ ///
+ /// The key to look for.
+ /// True if the key is present, otherwise false.
+ public bool Contains(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return _dictionary.ContainsKey(key);
+ }
+
+ ///
+ /// Determines if specified key and value are present.
+ ///
+ /// The key to look for.
+ /// The value to look for when the key has been matched.
+ /// True if the key and value were found, else false.
+ public bool Contains(string key, string value)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return _dictionary.TryGetValue(key, out string actualValue) && actualValue == value;
+ }
+
+ ///
+ /// Removes the specified key and its associated value.
+ ///
+ /// The key to remove.
+ /// True if the key was removed, else false.
+ public bool Remove(string key)
+ {
+ if (key == null)
+ {
+ throw new ArgumentNullException(nameof(key));
+ }
+
+ return _dictionary.Remove(key);
+ }
+
+ private static string Encode(string str)
+ {
+ return str
+ .Replace("%", "%25")
+ .Replace(";", "%3B")
+ .Replace("=", "%3D");
+ }
+
+ private static string Decode(string str)
+ {
+ return str
+ .Replace("%25", "%")
+ .Replace("%3B", ";")
+ .Replace("%3D", "=");
+ }
+
+ ///
+ /// Parses a string that was generated using ToastArguments into a object.
+ ///
+ /// The toast arguments string to deserialize.
+ /// The parsed toast arguments.
+ public static ToastArguments Parse(string toastArgumentsStr)
+ {
+ if (string.IsNullOrWhiteSpace(toastArgumentsStr))
+ {
+ return new ToastArguments();
+ }
+
+ string[] pairs = toastArgumentsStr.Split(';');
+
+ ToastArguments answer = new ToastArguments();
+
+ foreach (string pair in pairs)
+ {
+ string name;
+ string value;
+
+ int indexOfEquals = pair.IndexOf('=');
+
+ if (indexOfEquals == -1)
+ {
+ name = Decode(pair);
+ value = null;
+ }
+ else
+ {
+ name = Decode(pair.Substring(0, indexOfEquals));
+ value = Decode(pair.Substring(indexOfEquals + 1));
+ }
+
+ answer.Add(name, value);
+ }
+
+ return answer;
+ }
+
+ ///
+ /// Serializes the key-value pairs into a string that can be used within a toast notification.
+ ///
+ /// A string that can be used within a toast notification.
+ public sealed override string ToString()
+ {
+ return string.Join(Separator, this.Select(pair => EncodePair(pair.Key, pair.Value)));
+ }
+
+ internal static string EncodePair(string key, string value)
+ {
+ // Key
+ return Encode(key) +
+
+ // 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.
+ ///
+ /// An enumeartor of the key/value pairs.
+ public IEnumerator> GetEnumerator()
+ {
+ return _dictionary.GetEnumerator();
+ }
+
+ ///
+ /// Gets an enumerator to enumerate the query string parameters.
+ ///
+ /// An enumeartor of the key/value pairs.
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/ToastAudio.cs b/components/Notifications/src/Toasts/ToastAudio.cs
new file mode 100644
index 000000000..1f7f3a949
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastAudio.cs
@@ -0,0 +1,39 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Specify audio to be played when the Toast notification is received.
+ ///
+ public sealed class ToastAudio
+ {
+ ///
+ /// Gets or sets the media file to play in place of the default sound.
+ ///
+ public Uri Src { get; set; }
+
+ ///
+ /// Gets or sets a value indicating whether sound should repeat as long as the Toast is shown; false to play only once (default).
+ ///
+ public bool Loop { get; set; } = Element_ToastAudio.DEFAULT_LOOP;
+
+ ///
+ /// Gets or sets a value indicating whether sound is muted; false to allow the Toast notification sound to play (default).
+ ///
+ public bool Silent { get; set; } = Element_ToastAudio.DEFAULT_SILENT;
+
+ internal Element_ToastAudio ConvertToElement()
+ {
+ return new Element_ToastAudio()
+ {
+ Src = Src,
+ Loop = Loop,
+ Silent = Silent
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastBindingGeneric.cs b/components/Notifications/src/Toasts/ToastBindingGeneric.cs
new file mode 100644
index 000000000..7ea818807
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastBindingGeneric.cs
@@ -0,0 +1,99 @@
+// 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 CommunityToolkit.Notifications.Adaptive;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Generic Toast binding, where you provide text, images, and other visual elements for your Toast notification.
+ ///
+ public sealed class ToastBindingGeneric
+ {
+ ///
+ /// Gets the contents of the body of the Toast, which can include , ,
+ /// and (added in Anniversary Update). Also, elements must come
+ /// before any other elements. If an element is placed after any other element, an exception
+ /// will be thrown when you try to retrieve the Toast XML content. And finally, certain properties
+ /// like HintStyle aren't supported on the root children text elements, and only work inside an .
+ /// If you use on devices without the Anniversary Update, the group content will simply be dropped.
+ ///
+ public IList Children { get; private set; } = new List();
+
+ ///
+ /// Gets or sets an optional override of the logo displayed on the Toast notification.
+ ///
+ public ToastGenericAppLogo AppLogoOverride { get; set; }
+
+ ///
+ /// Gets or sets an optional hero image (a visually impactful image displayed on the Toast notification).
+ /// On devices without the Anniversary Update, the hero image will simply be ignored.
+ ///
+ public ToastGenericHeroImage HeroImage { get; set; }
+
+ ///
+ /// Gets or sets an optional text element that is displayed as attribution text. On devices without
+ /// the Anniversary Update, this text will appear as if it's another
+ /// element at the end of your Children list.
+ ///
+ public ToastGenericAttributionText Attribution { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as BCP-47 language tags such as "en-US"
+ /// or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is
+ /// a literal string, this attribute defaults to the user's UI language. If this value is a string reference,
+ /// this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Gets or sets a default base URI that is combined with relative URIs in image source attributes.
+ ///
+ public Uri BaseUri { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Toast notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ internal Element_ToastBinding ConvertToElement()
+ {
+ Element_ToastBinding binding = new Element_ToastBinding(ToastTemplateType.ToastGeneric)
+ {
+ BaseUri = BaseUri,
+ AddImageQuery = AddImageQuery,
+ Language = Language
+ };
+
+ // Add children
+ foreach (var child in Children)
+ {
+ var el = (IElement_ToastBindingChild)AdaptiveHelper.ConvertToElement(child);
+ binding.Children.Add(el);
+ }
+
+ // Add attribution
+ if (Attribution != null)
+ {
+ binding.Children.Add(Attribution.ConvertToElement());
+ }
+
+ // If there's hero, add it
+ if (HeroImage != null)
+ {
+ binding.Children.Add(HeroImage.ConvertToElement());
+ }
+
+ // If there's app logo, add it
+ if (AppLogoOverride != null)
+ {
+ binding.Children.Add(AppLogoOverride.ConvertToElement());
+ }
+
+ return binding;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastBindingShoulderTap.cs b/components/Notifications/src/Toasts/ToastBindingShoulderTap.cs
new file mode 100644
index 000000000..0097eb721
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastBindingShoulderTap.cs
@@ -0,0 +1,56 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Specifies content you want to appear in a My People shoulder tap notification. For more info, see the My People notifications documentation. New in Fall Creators Update.
+ ///
+ public sealed class ToastBindingShoulderTap
+ {
+ ///
+ /// Gets or sets the image to be displayed in the shoulder tap notification. Required.
+ ///
+ public ToastShoulderTapImage Image { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as BCP-47 language tags such as "en-US"
+ /// or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is
+ /// a literal string, this attribute defaults to the user's UI language. If this value is a string reference,
+ /// this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Gets or sets a default base URI that is combined with relative URIs in image source attributes.
+ ///
+ public Uri BaseUri { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Toast notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ internal Element_ToastBinding ConvertToElement()
+ {
+ Element_ToastBinding binding = new Element_ToastBinding(ToastTemplateType.ToastGeneric)
+ {
+ ExperienceType = "shoulderTap",
+ BaseUri = BaseUri,
+ AddImageQuery = AddImageQuery,
+ Language = Language
+ };
+
+ // If there's an image, add it
+ if (Image != null)
+ {
+ binding.Children.Add(Image.ConvertToElement());
+ }
+
+ return binding;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastButton.cs b/components/Notifications/src/Toasts/ToastButton.cs
new file mode 100644
index 000000000..e0db4f7cf
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastButton.cs
@@ -0,0 +1,446 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// A button that the user can click on a Toast notification.
+ ///
+ public sealed class ToastButton :
+ IToastActivateableBuilder,
+ IToastButton
+ {
+ private Dictionary _arguments = new Dictionary();
+
+ private bool _usingCustomArguments;
+
+ private bool _usingSnoozeActivation;
+ private string _snoozeSelectionBoxId;
+
+ private bool _usingDismissActivation;
+
+ internal bool NeedsContent()
+ {
+ // Snooze/dismiss buttons don't need content (the system will auto-add the localized strings).
+ return !_usingDismissActivation && !_usingSnoozeActivation;
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The text to display on the button.
+ /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the button.
+ public ToastButton(string content, string arguments)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException(nameof(content));
+ }
+
+ if (arguments == null)
+ {
+ throw new ArgumentNullException(nameof(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;
+ }
+
+ ///
+ /// Gets the text to display on the button. Required
+ ///
+ public string Content { get; private set; }
+
+ ///
+ /// 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; internal set; }
+
+ ///
+ /// Gets or sets what type of activation this button will use when clicked. Defaults to Foreground.
+ ///
+ public ToastActivationType ActivationType { get; set; } = ToastActivationType.Foreground;
+
+ ///
+ /// Gets or sets additional options relating to activation of the toast button. New in Creators Update
+ ///
+ public ToastActivationOptions ActivationOptions { get; set; }
+
+ ///
+ /// Gets or sets an optional image icon for the button to display (required for buttons adjacent to inputs like quick reply).
+ ///
+ public string ImageUri { get; set; }
+
+ ///
+ /// Gets or sets the ID of an existing in order to have this button display
+ /// to the right of the input, achieving a quick reply scenario.
+ ///
+ public string TextBoxId { get; set; }
+
+ ///
+ /// Gets or 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.
+ ///
+ 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
+ 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
+ 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
+ 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
+ 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
+ public ToastButton AddArgument(string key, bool value)
+ {
+ return AddArgumentHelper(key, value ? "1" : "0"); // Encode as 1 or 0 to save string space
+ }
+
+ ///
+ /// 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());
+ }
+
+ 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.");
+ }
+
+ if (_usingDismissActivation || _usingSnoozeActivation)
+ {
+ throw new InvalidOperationException("You cannot use the AddArgument methods when using dismiss or snooze 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;
+ }
+
+ ///
+ /// Configures the button to use system snooze activation when the button is clicked, using the default system snooze time.
+ ///
+ /// The current instance of
+ public ToastButton SetSnoozeActivation()
+ {
+ return SetSnoozeActivation(null);
+ }
+
+ ///
+ /// Configures the button to use system snooze activation when the button is clicked, with a snooze time defined by the specified selection box.
+ ///
+ /// The ID of an existing which allows the user to pick a custom snooze time. The ID's of the s inside the selection box must represent the snooze interval in minutes. For example, if the user selects an item that has an ID of "120", then the notification will be snoozed for 2 hours. When the user clicks this button, if you specified a SelectionBoxId, the system will parse the ID of the selected item and snooze by that amount of minutes.
+ /// The current instance of
+ public ToastButton SetSnoozeActivation(string selectionBoxId)
+ {
+ if (_arguments.Count > 0)
+ {
+ throw new InvalidOperationException($"{nameof(SetSnoozeActivation)} cannot be used in conjunction with ${nameof(AddArgument)}.");
+ }
+
+ _usingSnoozeActivation = true;
+ _snoozeSelectionBoxId = selectionBoxId;
+
+ return this;
+ }
+
+ ///
+ /// Configures the button to use system dismiss activation when the button is clicked (the toast will simply dismiss rather than activating).
+ ///
+ /// The current instance of
+ public ToastButton SetDismissActivation()
+ {
+ if (_arguments.Count > 0)
+ {
+ throw new InvalidOperationException($"{nameof(SetDismissActivation)} cannot be used in conjunction with ${nameof(AddArgument)}.");
+ }
+
+ _usingDismissActivation = true;
+ 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 && !_usingDismissActivation && !_usingSnoozeActivation;
+ }
+
+ internal bool ContainsArgument(string key)
+ {
+ return _arguments.ContainsKey(key);
+ }
+
+ internal Element_ToastAction ConvertToElement()
+ {
+ var el = new Element_ToastAction()
+ {
+ Content = Content,
+ ImageUri = ImageUri,
+ InputId = TextBoxId,
+ HintActionId = HintActionId
+ };
+
+ if (_usingSnoozeActivation)
+ {
+ el.ActivationType = Element_ToastActivationType.System;
+ el.Arguments = "snooze";
+
+ if (_snoozeSelectionBoxId != null)
+ {
+ el.InputId = _snoozeSelectionBoxId;
+ }
+
+ // Content needs to be specified as empty for auto-generated Snooze content
+ if (el.Content == null)
+ {
+ el.Content = string.Empty;
+ }
+ }
+ else if (_usingDismissActivation)
+ {
+ el.ActivationType = Element_ToastActivationType.System;
+ el.Arguments = "dismiss";
+
+ // Content needs to be specified as empty for auto-generated Dismiss content
+ if (el.Content == null)
+ {
+ el.Content = string.Empty;
+ }
+ }
+ else
+ {
+ el.ActivationType = Element_Toast.ConvertActivationType(ActivationType);
+ el.Arguments = Arguments;
+ }
+
+ ActivationOptions?.PopulateElement(el);
+
+ return el;
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/ToastButtonDismiss.cs b/components/Notifications/src/Toasts/ToastButtonDismiss.cs
new file mode 100644
index 000000000..9a92683b5
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastButtonDismiss.cs
@@ -0,0 +1,67 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A button that, when clicked, is interpreted as a "dismiss" by the system, and the Toast is dismissed just like if the user swiped the Toast away.
+ ///
+ public sealed class ToastButtonDismiss : IToastButton
+ {
+ ///
+ /// Gets custom text displayed on the button that overrides the default localized "Dismiss" text.
+ ///
+ public string CustomContent { get; private set; }
+
+ ///
+ /// Gets or sets an optional image icon for the button to display.
+ ///
+ public string ImageUri { get; set; }
+
+ ///
+ /// Gets or 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.
+ ///
+ public string HintActionId { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ToastButtonDismiss()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Constructs a system-handled dismiss button that displays your text on the button.
+ ///
+ /// The text you want displayed on the button.
+ public ToastButtonDismiss(string customContent)
+ {
+ if (customContent == null)
+ {
+ throw new ArgumentNullException(nameof(customContent));
+ }
+
+ CustomContent = customContent;
+ }
+
+ internal Element_ToastAction ConvertToElement()
+ {
+ return new Element_ToastAction()
+ {
+ Content = this.CustomContent == null ? string.Empty : this.CustomContent, // If not using custom content, we need to provide empty string, otherwise Toast doesn't get displayed
+ Arguments = "dismiss",
+ ActivationType = Element_ToastActivationType.System,
+ ImageUri = ImageUri,
+ HintActionId = HintActionId
+
+ // InputId is useless since dismiss button can't be placed to the right of text box (shell doesn't display it)
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastButtonSnooze.cs b/components/Notifications/src/Toasts/ToastButtonSnooze.cs
new file mode 100644
index 000000000..f2f29283a
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastButtonSnooze.cs
@@ -0,0 +1,77 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A system-handled snooze button that automatically handles snoozing of a Toast notification.
+ ///
+ public sealed class ToastButtonSnooze : IToastButton
+ {
+ ///
+ /// Gets custom text displayed on the button that overrides the default localized "Snooze" text.
+ ///
+ public string CustomContent { get; private set; }
+
+ ///
+ /// Gets or sets an optional image icon for the button to display.
+ ///
+ public string ImageUri { get; set; }
+
+ ///
+ /// Gets or sets the ID of an existing in order to allow the
+ /// user to pick a custom snooze time. Optional. The ID's of the s
+ /// inside the selection box must represent the snooze interval in minutes. For example,
+ /// if the user selects an item that has an ID of "120", then the notification will be snoozed
+ /// for 2 hours. When the user clicks this button, if you specified a SelectionBoxId, the system
+ /// will parse the ID of the selected item and snooze by that amount of minutes. If you didn't specify
+ /// a SelectionBoxId, the system will snooze by the default system snooze time.
+ ///
+ public string SelectionBoxId { get; set; }
+
+ ///
+ /// Gets or 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.
+ ///
+ public string HintActionId { get; set; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public ToastButtonSnooze()
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class.
+ /// Initializes a system-handled snooze button that displays your text on the button and automatically handles snoozing.
+ ///
+ /// The text you want displayed on the button.
+ public ToastButtonSnooze(string customContent)
+ {
+ if (customContent == null)
+ {
+ throw new ArgumentNullException(nameof(customContent));
+ }
+
+ CustomContent = customContent;
+ }
+
+ internal Element_ToastAction ConvertToElement()
+ {
+ return new Element_ToastAction()
+ {
+ Content = CustomContent ?? string.Empty, // If not using custom content, we need to provide empty string, otherwise Toast doesn't get displayed
+ Arguments = "snooze",
+ ActivationType = Element_ToastActivationType.System,
+ InputId = SelectionBoxId,
+ ImageUri = ImageUri,
+ HintActionId = HintActionId
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastCommon.cs b/components/Notifications/src/Toasts/ToastCommon.cs
new file mode 100644
index 000000000..088ec6ca0
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastCommon.cs
@@ -0,0 +1,43 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Decides the type of activation that will be used when the user interacts with the Toast notification.
+ ///
+ public enum ToastActivationType
+ {
+ ///
+ /// Default value. Your foreground app is launched.
+ ///
+ Foreground,
+
+ ///
+ /// Your corresponding background task (assuming you set everything up) is triggered, and you can execute code in the background (like sending the user's quick reply message) without interrupting the user.
+ ///
+ Background,
+
+ ///
+ /// Launch a different app using protocol activation.
+ ///
+ Protocol
+ }
+
+ ///
+ /// Specifies the behavior that the toast should use when the user takes action on the toast.
+ ///
+ public enum ToastAfterActivationBehavior
+ {
+ ///
+ /// Default behavior. The toast will be dismissed when the user takes action on the toast.
+ ///
+ Default,
+
+ ///
+ /// After the user clicks a button on your toast, the notification will remain present, in a "pending update" visual state. You should immediately update your toast from a background task so that the user does not see this "pending update" visual state for too long.
+ ///
+ PendingUpdate
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastContent.cs b/components/Notifications/src/Toasts/ToastContent.cs
new file mode 100644
index 000000000..d361cca3e
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastContent.cs
@@ -0,0 +1,187 @@
+// 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 Windows.Data.Xml.Dom;
+using Windows.UI.Notifications;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Base Toast element, which contains at least a visual element.
+ ///
+ public sealed class ToastContent : INotificationContent
+ {
+ ///
+ /// Gets or sets the visual element (Required).
+ ///
+ public ToastVisual Visual { get; set; }
+
+ ///
+ /// Gets or sets custom audio options.
+ ///
+ public ToastAudio Audio { get; set; }
+
+ ///
+ /// Gets or sets optional custom actions with buttons and inputs (using )
+ /// or optionally use the system-default snooze/dismiss controls (with ).
+ ///
+ public IToastActions Actions { get; set; }
+
+ ///
+ /// Gets or sets an optional header for the toast notification. Requires Creators Update
+ ///
+ public ToastHeader Header { get; set; }
+
+ ///
+ /// Gets or sets the scenario, to make the Toast behave like an alarm, reminder, or more.
+ ///
+ public ToastScenario Scenario { get; set; }
+
+ ///
+ /// Gets or sets the amount of time the Toast should display. You typically should use the
+ /// Scenario attribute instead, which impacts how long a Toast stays on screen.
+ ///
+ public ToastDuration Duration { get; set; }
+
+ ///
+ /// Gets or sets a string that is passed to the application when it is activated by the Toast.
+ /// The format and contents of this string are defined by the app for its own use. When the user
+ /// taps or clicks the Toast to launch its associated app, the launch string provides the context
+ /// to the app that allows it to show the user a view relevant to the Toast content, rather than
+ /// launching in its default way.
+ ///
+ public string Launch { get; set; }
+
+ ///
+ /// Gets or sets what activation type will be used when the user clicks the body of this Toast.
+ ///
+ public ToastActivationType ActivationType { get; set; }
+
+ ///
+ /// Gets or sets additional options relating to activation of the toast notification. Requires Creators Updated
+ ///
+ public ToastActivationOptions ActivationOptions { get; set; }
+
+ ///
+ /// Gets or sets an optional custom time to use for the notification's timestamp, visible within Action Center.
+ /// If provided, this date/time will be used on the notification instead of the date/time that the notification was received.
+ /// Requires Creators Update
+ ///
+ public DateTimeOffset? DisplayTimestamp { get; set; }
+
+ ///
+ /// Gets or sets an identifier used in telemetry to identify your category of toast notification. This should be something
+ /// like "NewMessage", "AppointmentReminder", "Promo30Off", or "PleaseRate". In the upcoming toast telemetry dashboard
+ /// in Dev Center, you will be able to view activation info filtered by toast identifier.
+ ///
+ public string HintToastId { get; set; }
+
+ ///
+ /// Gets or sets the person that this toast is related to. For more info, see the My People documentation. New in Fall Creators Update.
+ ///
+ public ToastPeople HintPeople { get; set; }
+
+ ///
+ /// Gets a dictionary where you can assign additional properties.
+ ///
+ public IDictionary AdditionalProperties { get; } = new Dictionary();
+
+ ///
+ /// Retrieves the notification XML content as a string, so that it can be sent with a HTTP POST in a push notification.
+ ///
+ /// The notification XML content as a string.
+ public string GetContent()
+ {
+ return ConvertToElement().GetContent();
+ }
+
+
+ ///
+ /// Retrieves the notification XML content as a WinRT XmlDocument, so that it can be used with a local Toast notification's constructor on either or .
+ ///
+ /// The notification XML content as a WinRT XmlDocument.
+ public XmlDocument GetXml()
+ {
+ XmlDocument doc = new XmlDocument();
+ doc.LoadXml(GetContent());
+
+ return doc;
+ }
+
+ internal Element_Toast ConvertToElement()
+ {
+ if (ActivationOptions != null)
+ {
+ if (ActivationOptions.AfterActivationBehavior != ToastAfterActivationBehavior.Default)
+ {
+ throw new InvalidOperationException("ToastContent does not support a custom AfterActivationBehavior. Please ensure ActivationOptions.AfterActivationBehavior is set to Default.");
+ }
+ }
+
+ DateTimeOffset? strippedDisplayTimestamp = null;
+ if (DisplayTimestamp != null)
+ {
+ // We need to make sure we don't include more than 3 decimal points on seconds
+ // The Millisecond value itself is limited to 3 decimal points, thus by doing the following
+ // we bypass the more granular value that can come from Ticks and ensure we only have 3 decimals at most.
+ var val = DisplayTimestamp.Value;
+ strippedDisplayTimestamp = new DateTimeOffset(val.Year, val.Month, val.Day, val.Hour, val.Minute, val.Second, val.Millisecond, val.Offset);
+ }
+
+ var toast = new Element_Toast()
+ {
+ ActivationType = Element_Toast.ConvertActivationType(ActivationType),
+ Duration = Duration,
+ Launch = Launch,
+ Scenario = Scenario,
+ DisplayTimestamp = strippedDisplayTimestamp,
+ HintToastId = HintToastId,
+ AdditionalProperties = (Dictionary)AdditionalProperties
+ };
+
+ ActivationOptions?.PopulateElement(toast);
+
+ if (Visual != null)
+ {
+ toast.Visual = Visual.ConvertToElement();
+ }
+
+ if (Audio != null)
+ {
+ toast.Audio = Audio.ConvertToElement();
+ }
+
+ if (Actions != null)
+ {
+ toast.Actions = ConvertToActionsElement(Actions);
+ }
+
+ if (Header != null)
+ {
+ toast.Header = Header.ConvertToElement();
+ }
+
+ HintPeople?.PopulateToastElement(toast);
+
+ return toast;
+ }
+
+ private static Element_ToastActions ConvertToActionsElement(IToastActions actions)
+ {
+ if (actions is ToastActionsCustom)
+ {
+ return (actions as ToastActionsCustom).ConvertToElement();
+ }
+
+ if (actions is ToastActionsSnoozeAndDismiss)
+ {
+ return (actions as ToastActionsSnoozeAndDismiss).ConvertToElement();
+ }
+
+ throw new NotImplementedException("Unknown actions type: " + actions.GetType());
+ }
+ }
+}
diff --git a/components/Notifications/src/Toasts/ToastContextMenuItem.cs b/components/Notifications/src/Toasts/ToastContextMenuItem.cs
new file mode 100644
index 000000000..a98b1a9e4
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastContextMenuItem.cs
@@ -0,0 +1,79 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A Toast context menu item.
+ ///
+ public sealed class ToastContextMenuItem
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// A Toast context menu item with the required properties.
+ ///
+ /// The text to display on the menu item.
+ /// App-defined string of arguments that the app can later retrieve once it is activated when the user clicks the menu item.
+ public ToastContextMenuItem(string content, string arguments)
+ {
+ if (content == null)
+ {
+ throw new ArgumentNullException(nameof(content));
+ }
+
+ if (arguments == null)
+ {
+ throw new ArgumentNullException(nameof(arguments));
+ }
+
+ Content = content;
+ Arguments = arguments;
+ }
+
+ ///
+ /// Gets the text to display on the menu item. Required
+ ///
+ public string Content { get; private set; }
+
+ ///
+ /// Gets app-defined string of arguments that the app can later retrieve once it is activated when the user clicks the menu item. Required
+ ///
+ public string Arguments { get; private set; }
+
+ ///
+ /// Gets or sets what type of activation this menu item will use when clicked. Defaults to Foreground.
+ ///
+ public ToastActivationType ActivationType { get; set; } = ToastActivationType.Foreground;
+
+ ///
+ /// Gets or sets additional options relating to activation of the toast context menu item. New in Creators Update
+ ///
+ public ToastActivationOptions ActivationOptions { get; set; }
+
+ ///
+ /// Gets or sets an identifier used in telemetry to identify your category of action. This should be something
+ /// like "TurnOff" or "ManageSettings". In the upcoming toast telemetry dashboard in Dev Center, you will
+ /// be able to view how frequently your actions are being clicked.
+ ///
+ public string HintActionId { get; set; }
+
+ internal Element_ToastAction ConvertToElement()
+ {
+ var el = new Element_ToastAction
+ {
+ Content = Content,
+ Arguments = Arguments,
+ ActivationType = Element_Toast.ConvertActivationType(ActivationType),
+ Placement = Element_ToastActionPlacement.ContextMenu,
+ HintActionId = HintActionId
+ };
+
+ ActivationOptions?.PopulateElement(el);
+
+ return el;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastGenericAppLogo.cs b/components/Notifications/src/Toasts/ToastGenericAppLogo.cs
new file mode 100644
index 000000000..c2c8fd0b7
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastGenericAppLogo.cs
@@ -0,0 +1,73 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// The logo that is displayed on your Toast notification.
+ ///
+ public sealed class ToastGenericAppLogo : IBaseImage
+ {
+ ///
+ /// Initializes a new instance of the class,
+ /// a logo that is displayed on your Toast notification.
+ ///
+ public ToastGenericAppLogo()
+ {
+ }
+
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ public string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets set a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ ///
+ /// Gets or sets specify how the image should be cropped.
+ ///
+ public ToastGenericAppLogoCrop HintCrop { get; set; }
+
+ internal Element_AdaptiveImage ConvertToElement()
+ {
+ Element_AdaptiveImage el = BaseImageHelper.CreateBaseElement(this);
+
+ el.Placement = AdaptiveImagePlacement.AppLogoOverride;
+ el.Crop = GetAdaptiveImageCrop();
+
+ return el;
+ }
+
+ private AdaptiveImageCrop GetAdaptiveImageCrop()
+ {
+ switch (HintCrop)
+ {
+ case ToastGenericAppLogoCrop.Circle:
+ return AdaptiveImageCrop.Circle;
+
+ case ToastGenericAppLogoCrop.None:
+ return AdaptiveImageCrop.None;
+
+ default:
+ return AdaptiveImageCrop.Default;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastGenericAppLogoEnums.cs b/components/Notifications/src/Toasts/ToastGenericAppLogoEnums.cs
new file mode 100644
index 000000000..b8f79aa7f
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastGenericAppLogoEnums.cs
@@ -0,0 +1,27 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Specify the desired cropping of the image.
+ ///
+ public enum ToastGenericAppLogoCrop
+ {
+ ///
+ /// Cropping uses the default behavior of the renderer.
+ ///
+ Default,
+
+ ///
+ /// Image is not cropped.
+ ///
+ None,
+
+ ///
+ /// Image is cropped to a circle shape.
+ ///
+ Circle
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastGenericAttributionText.cs b/components/Notifications/src/Toasts/ToastGenericAttributionText.cs
new file mode 100644
index 000000000..b3ec7d2dc
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastGenericAttributionText.cs
@@ -0,0 +1,41 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Defines an attribution text element to be displayed on the Toast notification.
+ ///
+ public sealed class ToastGenericAttributionText : IBaseText
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// An attribution text element to be displayed on the Toast notification.
+ ///
+ public ToastGenericAttributionText()
+ {
+ }
+
+ ///
+ /// Gets or sets the text to display.
+ ///
+ public string Text { get; set; }
+
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as a BCP-47 language tags such as "en-US" or "fr-FR". The locale specified here overrides any other specified locale, such as that in binding or visual. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ public string Language { get; set; }
+
+ internal Element_AdaptiveText ConvertToElement()
+ {
+ var el = BaseTextHelper.CreateBaseElement(this);
+
+ el.Placement = AdaptiveTextPlacement.Attribution;
+
+ return el;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastGenericHeroImage.cs b/components/Notifications/src/Toasts/ToastGenericHeroImage.cs
new file mode 100644
index 000000000..adbb18809
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastGenericHeroImage.cs
@@ -0,0 +1,52 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// A hero image for the Toast notification.
+ ///
+ public sealed class ToastGenericHeroImage : IBaseImage
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// A hero image for the Toast notification.
+ ///
+ public ToastGenericHeroImage()
+ {
+ }
+
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the image. Can be from your application package, application data, or the internet. Internet images must be less than 200 KB in size.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ public string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Tile notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ internal Element_AdaptiveImage ConvertToElement()
+ {
+ Element_AdaptiveImage el = BaseImageHelper.CreateBaseElement(this);
+
+ el.Placement = AdaptiveImagePlacement.Hero;
+
+ return el;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastHeader.cs b/components/Notifications/src/Toasts/ToastHeader.cs
new file mode 100644
index 000000000..2f9b9dabd
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastHeader.cs
@@ -0,0 +1,116 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Defines a visual header for the toast notification.
+ ///
+ public sealed class ToastHeader
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Constructs a toast header with all the required properties.
+ ///
+ /// 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.
+ /// A developer-defined string of arguments that is returned to the app when the user clicks this header.
+ public ToastHeader(string id, string title, string arguments)
+ {
+ Id = id;
+ Title = title;
+ Arguments = arguments;
+ }
+
+ private string _id;
+
+ ///
+ /// Gets or sets 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. Cannot be null.
+ ///
+ public string Id
+ {
+ get { return _id; }
+ set { ArgumentValidator.SetProperty(ref _id, value, nameof(Id), ArgumentValidatorOptions.NotNull); }
+ }
+
+ private string _title;
+
+ ///
+ /// Gets or sets a title for the header. Cannot be null.
+ ///
+ public string Title
+ {
+ get { return _title; }
+ set { ArgumentValidator.SetProperty(ref _title, value, nameof(Title), ArgumentValidatorOptions.NotNull); }
+ }
+
+ private string _arguments;
+
+ ///
+ /// Gets or sets a developer-defined string of arguments that is returned to the app when the user clicks this header. Cannot be null.
+ ///
+ public string Arguments
+ {
+ get { return _arguments; }
+ set { ArgumentValidator.SetProperty(ref _arguments, value, nameof(Arguments), ArgumentValidatorOptions.NotNull); }
+ }
+
+ private ToastActivationType _activationType = ToastActivationType.Foreground;
+
+ ///
+ /// Gets or sets the type of activation this header will use when clicked. Defaults to Foreground. Note that only Foreground and Protocol are supported.
+ ///
+ public ToastActivationType ActivationType
+ {
+ get
+ {
+ return _activationType;
+ }
+
+ set
+ {
+ switch (value)
+ {
+ case ToastActivationType.Foreground:
+ case ToastActivationType.Protocol:
+ _activationType = value;
+ break;
+
+ default:
+ throw new ArgumentException($"ActivationType of {value} is not supported on ToastHeader.");
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets additional options relating to activation of the toast header. New in Creators Update
+ ///
+ public ToastActivationOptions ActivationOptions { get; set; }
+
+ internal Element_ToastHeader ConvertToElement()
+ {
+ if (ActivationOptions != null)
+ {
+ if (ActivationOptions.AfterActivationBehavior != ToastAfterActivationBehavior.Default)
+ {
+ throw new InvalidOperationException("ToastHeader does not support a custom AfterActivationBehavior. Please ensure ActivationOptions.AfterActivationBehavior is set to Default.");
+ }
+ }
+
+ var el = new Element_ToastHeader()
+ {
+ Id = Id,
+ Title = Title,
+ Arguments = Arguments,
+ ActivationType = Element_Toast.ConvertActivationType(ActivationType)
+ };
+
+ ActivationOptions?.PopulateElement(el);
+
+ return el;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastPeople.cs b/components/Notifications/src/Toasts/ToastPeople.cs
new file mode 100644
index 000000000..3c24740b4
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastPeople.cs
@@ -0,0 +1,56 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Specify what person this toast is related to. For more info, see the My People documentation. New in Fall Creators Update.
+ ///
+ public sealed class ToastPeople
+ {
+ ///
+ /// Gets or sets a remote identifier that corresponds with the RemoteId you set on a Contact you created
+ /// with the ContactStore APIs. For more info, see the My People documentation.
+ ///
+ public string RemoteId { get; set; }
+
+ ///
+ /// Gets or sets an email address that corresponds with a contact in the local Windows ContactStore. Note
+ /// that if is specified, this property will be ignored. For more info,
+ /// see the My People documentation.
+ ///
+ public string EmailAddress { get; set; }
+
+ ///
+ /// Gets or sets a phone number that corresponds with a contact in the local Windows ContactStore. Note
+ /// that if is specified, this property will be ignored.
+ /// For more info, see the My People documentation.
+ ///
+ public string PhoneNumber { get; set; }
+
+ internal void PopulateToastElement(Element_Toast toast)
+ {
+ string hintPeople;
+
+ if (RemoteId != null)
+ {
+ hintPeople = "remoteid:" + RemoteId;
+ }
+ else if (EmailAddress != null)
+ {
+ hintPeople = "mailto:" + EmailAddress;
+ }
+ else if (PhoneNumber != null)
+ {
+ hintPeople = "tel:" + PhoneNumber;
+ }
+ else
+ {
+ return;
+ }
+
+ toast.HintPeople = hintPeople;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastSelectionBox.cs b/components/Notifications/src/Toasts/ToastSelectionBox.cs
new file mode 100644
index 000000000..40d110057
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastSelectionBox.cs
@@ -0,0 +1,63 @@
+// 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;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// A selection box control, which lets users pick from a dropdown list of options.
+ ///
+ public sealed class ToastSelectionBox : IToastInput
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// A Toast SelectionBox input control with the required elements.
+ ///
+ /// Developer-provided ID that the developer uses later to retrieve input when the Toast is interacted with.
+ public ToastSelectionBox(string id)
+ {
+ Id = id ?? throw new ArgumentNullException(nameof(id));
+ }
+
+ ///
+ /// Gets the required ID property used so that developers can retrieve user input once the app is activated.
+ ///
+ public string Id { get; private set; }
+
+ ///
+ /// Gets or sets title text to display above the SelectionBox.
+ ///
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets which item is selected by default, and refers to the Id property of . If you do not provide this, the default selection will be empty (user sees nothing).
+ ///
+ public string DefaultSelectionBoxItemId { get; set; }
+
+ ///
+ /// Gets the selection items that the user can pick from in this SelectionBox. Only 5 items can be added.
+ ///
+ public IList Items { get; private set; } = new LimitedList(5);
+
+ internal Element_ToastInput ConvertToElement()
+ {
+ var input = new Element_ToastInput()
+ {
+ Type = ToastInputType.Selection,
+ Id = Id,
+ DefaultInput = DefaultSelectionBoxItemId,
+ Title = Title
+ };
+
+ foreach (var item in Items)
+ {
+ input.Children.Add(item.ConvertToElement());
+ }
+
+ return input;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastSelectionBoxItem.cs b/components/Notifications/src/Toasts/ToastSelectionBoxItem.cs
new file mode 100644
index 000000000..225a4f7ab
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastSelectionBoxItem.cs
@@ -0,0 +1,45 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A selection box item (an item that the user can select from the drop down list).
+ ///
+ public sealed class ToastSelectionBoxItem
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// Constructs a new Toast SelectionBoxItem with the required elements.
+ ///
+ /// Developer-provided ID that the developer uses later to retrieve input when the Toast is interacted with.
+ /// String that is displayed on the selection item. This is what the user sees.
+ public ToastSelectionBoxItem(string id, string content)
+ {
+ Id = id ?? throw new ArgumentNullException(nameof(id));
+ Content = content ?? throw new ArgumentNullException(nameof(content));
+ }
+
+ ///
+ /// Gets the required ID property used so that developers can retrieve user input once the app is activated.
+ ///
+ public string Id { get; private set; }
+
+ ///
+ /// Gets the required string that is displayed on the selection item.
+ ///
+ public string Content { get; private set; }
+
+ internal Element_ToastSelection ConvertToElement()
+ {
+ return new Element_ToastSelection()
+ {
+ Id = Id,
+ Content = Content
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastShoulderTapImage.cs b/components/Notifications/src/Toasts/ToastShoulderTapImage.cs
new file mode 100644
index 000000000..58a4e2a52
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastShoulderTapImage.cs
@@ -0,0 +1,57 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Specifies the image to be displayed on a My People shoulder tap notification. New in Fall Creators Update.
+ ///
+ public sealed class ToastShoulderTapImage : IBaseImage
+ {
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the image (Required). This will be used if the sprite sheet isn't provided, or
+ /// if the sprite sheet cannot be loaded. Can be from your application package, application data, or the internet.
+ /// Internet images must obey the toast image size restrictions.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets an optional sprite sheet that can be used instead of the image to display an animated sprite sheet.
+ ///
+ public ToastSpriteSheet SpriteSheet { get; set; }
+
+ ///
+ /// Gets or sets a description of the image, for users of assistive technologies.
+ ///
+ public string AlternateText { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows should append a query string to the image URI supplied in the property.
+ /// Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the
+ /// query strings or by ignoring the query string and returning the image as specified without the query string.
+ /// This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ internal Element_AdaptiveImage ConvertToElement()
+ {
+ Element_AdaptiveImage image = BaseImageHelper.CreateBaseElement(this);
+
+ if (SpriteSheet != null)
+ {
+ SpriteSheet.PopulateImageElement(image);
+ }
+
+ return image;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastSpriteSheet.cs b/components/Notifications/src/Toasts/ToastSpriteSheet.cs
new file mode 100644
index 000000000..b8e0941f9
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastSpriteSheet.cs
@@ -0,0 +1,56 @@
+// 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 CommunityToolkit.Notifications.Adaptive.Elements;
+
+namespace CommunityToolkit.Notifications
+{
+ ///
+ /// Specifies a sprite sheet. New in Fall Creators Update.
+ ///
+ public sealed class ToastSpriteSheet
+ {
+ private string _source;
+
+ ///
+ /// Gets or sets the URI of the sprite sheet (Required).
+ /// Can be from your application package, application data, or the internet.
+ /// Internet sources must obey the toast image size restrictions.
+ ///
+ public string Source
+ {
+ get { return _source; }
+ set { BaseImageHelper.SetSource(ref _source, value); }
+ }
+
+ ///
+ /// Gets or sets the frame-height of the sprite sheet. Required value that must be greater than 0.
+ ///
+ public uint? FrameHeight { get; set; }
+
+ ///
+ /// Gets or sets the frames per second at which to animate the sprite sheet. Required value that must be greater than 0 but no larger than 120.
+ ///
+ public uint? Fps { get; set; }
+
+ ///
+ /// Gets or sets the starting frame of the sprite sheet. If not specified, it will start at frame zero.
+ ///
+ public uint? StartingFrame { get; set; }
+
+ internal void PopulateImageElement(Element_AdaptiveImage image)
+ {
+ if (Source == null)
+ {
+ throw new NullReferenceException("Source cannot be null on ToastSpriteSheet");
+ }
+
+ image.SpriteSheetSrc = Source;
+ image.SpriteSheetHeight = FrameHeight;
+ image.SpriteSheetFps = Fps;
+ image.SpriteSheetStartingFrame = StartingFrame;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastTextBox.cs b/components/Notifications/src/Toasts/ToastTextBox.cs
new file mode 100644
index 000000000..432b75e70
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastTextBox.cs
@@ -0,0 +1,56 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// A text box control on the Toast that a user can type text into.
+ ///
+ public sealed class ToastTextBox : IToastInput
+ {
+ ///
+ /// Initializes a new instance of the class.
+ /// A new Toast TextBox input control with the required elements.
+ ///
+ /// Developer-provided ID that the developer uses later to retrieve input when the Toast is interacted with.
+ public ToastTextBox(string id)
+ {
+ Id = id ?? throw new ArgumentNullException(nameof(id));
+ }
+
+ ///
+ /// Gets the required ID property so that developers can retrieve user input once the app is activated.
+ ///
+ public string Id { get; private set; }
+
+ ///
+ /// Gets or sets title text to display above the text box.
+ ///
+ public string Title { get; set; }
+
+ ///
+ /// Gets or sets placeholder text to be displayed on the text box when the user hasn't typed any text yet.
+ ///
+ public string PlaceholderContent { get; set; }
+
+ ///
+ /// Gets or sets the initial text to place in the text box. Leave this null for a blank text box.
+ ///
+ public string DefaultInput { get; set; }
+
+ internal Element_ToastInput ConvertToElement()
+ {
+ return new Element_ToastInput()
+ {
+ Id = Id,
+ Type = ToastInputType.Text,
+ DefaultInput = DefaultInput,
+ PlaceholderContent = PlaceholderContent,
+ Title = Title
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/src/Toasts/ToastVisual.cs b/components/Notifications/src/Toasts/ToastVisual.cs
new file mode 100644
index 000000000..f6f349783
--- /dev/null
+++ b/components/Notifications/src/Toasts/ToastVisual.cs
@@ -0,0 +1,66 @@
+// 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 CommunityToolkit.Notifications
+{
+ ///
+ /// Defines the visual aspects of a Toast notification.
+ ///
+ public sealed class ToastVisual
+ {
+ ///
+ /// Gets or sets the target locale of the XML payload, specified as BCP-47 language tags such as "en-US" or "fr-FR". This locale is overridden by any locale specified in binding or text. If this value is a literal string, this attribute defaults to the user's UI language. If this value is a string reference, this attribute defaults to the locale chosen by Windows Runtime in resolving the string.
+ ///
+ public string Language { get; set; }
+
+ ///
+ /// Gets or sets a default base URI that is combined with relative URIs in image source attributes.
+ ///
+ public Uri BaseUri { get; set; }
+
+ ///
+ /// Gets or sets a value whether Windows is allowed to append a query string to the image URI supplied in the Toast notification. Use this attribute if your server hosts images and can handle query strings, either by retrieving an image variant based on the query strings or by ignoring the query string and returning the image as specified without the query string. This query string specifies scale, contrast setting, and language.
+ ///
+ public bool? AddImageQuery { get; set; }
+
+ ///
+ /// Gets or sets the generic Toast binding, which can be rendered on all devices. This binding is required and cannot be null.
+ ///
+ public ToastBindingGeneric BindingGeneric { get; set; }
+
+ ///
+ /// Gets or sets a binding for shoulder tap notifications, which integrate with My People. See the My People documentation for more info. New in Fall Creators Update.
+ ///
+ public ToastBindingShoulderTap BindingShoulderTap { get; set; }
+
+ internal Element_ToastVisual ConvertToElement()
+ {
+ var visual = new Element_ToastVisual()
+ {
+ Language = Language,
+ BaseUri = BaseUri,
+ AddImageQuery = AddImageQuery
+ };
+
+ if (BindingGeneric == null)
+ {
+ throw new NullReferenceException("BindingGeneric must be initialized");
+ }
+
+ Element_ToastBinding binding = BindingGeneric.ConvertToElement();
+
+ // TODO: If a BaseUri wasn't provided, we can potentially optimize the payload size by calculating the best BaseUri
+ visual.Bindings.Add(binding);
+
+ if (BindingShoulderTap != null)
+ {
+ visual.Bindings.Add(BindingShoulderTap.ConvertToElement());
+ }
+
+ return visual;
+ }
+ }
+}
\ No newline at end of file
diff --git a/components/Notifications/tests/Notifications.Tests.projitems b/components/Notifications/tests/Notifications.Tests.projitems
new file mode 100644
index 000000000..34ce1542c
--- /dev/null
+++ b/components/Notifications/tests/Notifications.Tests.projitems
@@ -0,0 +1,24 @@
+
+
+
+ $(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ true
+ 695EB17B-60C6-4D00-9D3F-1BC54B6A9500
+
+
+ NotificationsExperiment.Tests
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/components/Notifications/tests/Notifications.Tests.shproj b/components/Notifications/tests/Notifications.Tests.shproj
new file mode 100644
index 000000000..910121c08
--- /dev/null
+++ b/components/Notifications/tests/Notifications.Tests.shproj
@@ -0,0 +1,13 @@
+
+
+
+ 695EB17B-60C6-4D00-9D3F-1BC54B6A9500
+ 14.0
+
+
+
+
+
+
+
+
diff --git a/components/Notifications/tests/TestAssertHelper.cs b/components/Notifications/tests/TestAssertHelper.cs
new file mode 100644
index 000000000..aaef76806
--- /dev/null
+++ b/components/Notifications/tests/TestAssertHelper.cs
@@ -0,0 +1,454 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Xml;
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class TestAssertHelper
+ {
+ [TestMethod]
+ public void TestAssertXmlElement()
+ {
+ AssertHelper.AssertXml(" Hello world ", " Hello world ");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_002()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" Hello world ", " Hello world ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("tile element name was different, should have thrown exception");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_003()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" Hello world ", " Hello world ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("visual element name was incorrect, should have thrown exception");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_004()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" Hello world ", " Hello world ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("visual version number was incorrect, should have thrown exception");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_005()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" Hello world ", " Hello world! ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("text content was different, should have thrown exception");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_006()
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_006_1()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Version number was incorrect, should have thrown exception.");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_006_2()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("ID number was incorrect, should have thrown exception.");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_007()
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_008()
+ {
+ AssertHelper.AssertXml(" ", " ");
+
+ try
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Visual element was missing, should have thrown exception");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_009()
+ {
+ AssertHelper.AssertXml(" ", " ");
+
+ try
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Child elements were different order, should have thrown exception");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_010()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("id attribute value wasn't the same, should have thrown exception");
+ }
+
+ [TestMethod]
+ public void TestAssertXmlElement_011()
+ {
+ try
+ {
+ AssertHelper.AssertXml(" ", " ");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("id attribute was missing, should have thrown exception");
+ }
+ }
+
+#pragma warning disable SA1204 // Static elements should appear before instance elements
+#pragma warning disable SA1402 // File may only contain a single type
+ public static class AssertHelper
+#pragma warning restore SA1402 // File may only contain a single type
+#pragma warning restore SA1204 // Static elements should appear before instance elements
+ {
+ private class XmlElementHelper
+ {
+ }
+
+ /*
+ public static void AssertXml(string expected, string actual)
+ {
+ XmlDocument expectedDoc = new XmlDocument();
+ expectedDoc.LoadXml(expected);
+
+ XmlDocument actualDoc = new XmlDocument();
+ actualDoc.LoadXml(actual);
+
+ AssertXmlElement(expectedDoc.DocumentElement, actualDoc.DocumentElement);
+ }
+
+ private static void AssertXmlElement(XmlElement expected, XmlElement actual)
+ {
+ // If both null, good, done
+ if (expected == null && actual == null)
+ return;
+
+ // If one is null and other isn't, bad
+ if (expected == null)
+ Assert.Fail("Expected XML element was null, while actual was initialized");
+
+ if (actual == null)
+ Assert.Fail("Actual XML element was null, while expected was initialized");
+
+ // If name doesn't match
+ Assert.AreEqual(expected.Name, actual.Name, "Element names did not match.");
+
+ // If attribute count doesn't match
+ Assert.AreEqual(expected.Attributes.Count, actual.Attributes.Count, "Element attributes counts didn't match");
+
+ // Make sure attributes match (order does NOT matter)
+ foreach (XmlAttribute expectedAttr in expected.Attributes)
+ {
+ var actualAttr = actual.Attributes.GetNamedItem(expectedAttr.Name);
+
+ // If didn't find the attribute
+ if (actualAttr == null)
+ Assert.Fail("Expected element to have attribute " + expectedAttr.Name + " but it didn't.");
+
+ // Make sure value matches
+ Assert.AreEqual(expectedAttr.Value, actualAttr.Value, $@"Attribute values for ""{expectedAttr.Name}"" didn't match.");
+ }
+
+ // Make sure children elements match (order DOES matter)
+
+ // Obtain the child elements (ignore any comments, w
+ XmlElement[] expectedChildren = expected.ChildNodes.OfType().ToArray();
+ XmlElement[] actualChildren = actual.ChildNodes.OfType().ToArray();
+
+ Assert.AreEqual(expectedChildren.Length, actualChildren.Length, "Number of child elements did not match.");
+
+ // If no elements, compare inner text
+ if (expectedChildren.Length == 0)
+ {
+ Assert.AreEqual(expected.InnerText, actual.InnerText, "Inner text did not match.");
+ }
+
+ // Otherwise compare elements
+ else
+ {
+ for (int i = 0; i < expectedChildren.Length; i++)
+ {
+ AssertXmlElement(expectedChildren[i], actualChildren[i]);
+ }
+ }
+ }
+ */
+
+ public static void AssertToast(string expected, ToastContent toast)
+ {
+ AssertHelper.AssertXml(expected.ToLower(), toast.GetContent().ToLower());
+
+ AssertHelper.AssertXml(expected, toast.GetXml().GetXml());
+ }
+
+ public static void AssertTile(string expected, TileContent tile)
+ {
+ AssertHelper.AssertXml(expected.ToLower(), tile.GetContent().ToLower());
+
+ AssertHelper.AssertXml(expected, tile.GetXml().GetXml());
+ }
+
+ public static void AssertXml(string expected, string actual)
+ {
+ MyXmlElement expectedEl = ParseXml(expected);
+ MyXmlElement actualEl = ParseXml(actual);
+
+ AssertXmlElement(expectedEl, actualEl);
+ }
+
+ private static string AttributesToString(IEnumerable attributes)
+ {
+ return string.Join(", ", attributes.Select(i => i.Name));
+ }
+
+ private static void AssertXmlElement(MyXmlElement expected, MyXmlElement actual)
+ {
+ // If both null, good, done
+ if (expected == null && actual == null)
+ {
+ return;
+ }
+
+ // If one is null and other isn't, bad
+ if (expected == null)
+ {
+ Assert.Fail("Expected XML element was null, while actual was initialized");
+ }
+
+ if (actual == null)
+ {
+ Assert.Fail("Actual XML element was null, while expected was initialized");
+ }
+
+ // If name doesn't match
+ Assert.AreEqual(expected.Name.ToLower(), actual.Name.ToLower(), "Element names did not match.");
+
+ // If attribute count doesn't match
+ Assert.AreEqual(expected.Attributes.Count, actual.Attributes.Count, $"Different number of attributes on <{expected.Name}>\n\nExpected: " + AttributesToString(expected.Attributes) + "\nActual: " + AttributesToString(actual.Attributes));
+
+ // Make sure attributes match (order does NOT matter)
+ foreach (MyXmlAttribute expectedAttr in expected.Attributes)
+ {
+ var actualAttr = actual.Attributes.FirstOrDefault(i => i.Name.Equals(expectedAttr.Name));
+
+ // If didn't find the attribute
+ if (actualAttr == null)
+ {
+ Assert.Fail("Expected element to have attribute " + expectedAttr.Name + " but it didn't.");
+ }
+
+ // Make sure value matches
+ Assert.AreEqual(expectedAttr.Value.ToLower(), actualAttr.Value.ToLower(), $@"Attribute values for ""{expectedAttr.Name}"" didn't match.");
+ }
+
+ // Make sure children elements match (order DOES matter)
+
+ // Obtain the child elements (ignore any comments, w
+ MyXmlElement[] expectedChildren = expected.ChildNodes.ToArray();
+ MyXmlElement[] actualChildren = actual.ChildNodes.ToArray();
+
+ Assert.AreEqual(expectedChildren.Length, actualChildren.Length, "Number of child elements did not match.");
+
+ // Compare elements
+ for (int i = 0; i < expectedChildren.Length; i++)
+ {
+ AssertXmlElement(expectedChildren[i], actualChildren[i]);
+ }
+ }
+
+ private class MyXmlElement
+ {
+ public string Name { get; set; }
+
+ public List ChildNodes { get; private set; } = new List();
+
+ public List Attributes { get; private set; } = new List();
+ }
+
+ private class MyXmlAttribute
+ {
+ public string Name { get; set; }
+
+ public string Value { get; set; }
+ }
+
+ private static MyXmlElement ParseXml(string xml)
+ {
+ XmlReader reader = XmlReader.Create(new StringReader(xml));
+
+ MyXmlElement documentElement = new MyXmlElement();
+
+ reader.Read();
+
+ while (true)
+ {
+ if (reader.ReadState == ReadState.EndOfFile)
+ {
+ break;
+ }
+
+ if (reader.ReadState == ReadState.Error)
+ {
+ throw new Exception("ReadState was Error");
+ }
+
+ if (reader.NodeType == XmlNodeType.Element)
+ {
+ PopulateElement(reader, documentElement);
+ ParseXml(reader, documentElement);
+ break;
+ }
+
+ reader.Read();
+ }
+
+ return documentElement;
+ }
+
+ private static void PopulateElement(XmlReader reader, MyXmlElement into)
+ {
+ into.Name = reader.Name;
+
+ int attrCount = reader.AttributeCount;
+ for (int i = 0; i < attrCount; i++)
+ {
+ reader.MoveToNextAttribute();
+
+ into.Attributes.Add(new MyXmlAttribute()
+ {
+ Name = reader.Name,
+ Value = reader.Value
+ });
+ }
+ }
+
+ private static void ParseXml(XmlReader reader, MyXmlElement intoElement)
+ {
+ if (!reader.Read())
+ {
+ return;
+ }
+
+ while (true)
+ {
+ switch (reader.NodeType)
+ {
+ // Found child
+ case XmlNodeType.Element:
+ case XmlNodeType.Text:
+ MyXmlElement child = new MyXmlElement();
+ PopulateElement(reader, child);
+ ParseXml(reader, child);
+ intoElement.ChildNodes.Add(child);
+ break;
+
+ // All done
+ case XmlNodeType.EndElement:
+ return;
+ }
+
+ if (!reader.Read())
+ {
+ return;
+ }
+ }
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/TestMail.cs b/components/Notifications/tests/TestMail.cs
new file mode 100644
index 000000000..3d76ce9b1
--- /dev/null
+++ b/components/Notifications/tests/TestMail.cs
@@ -0,0 +1,171 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class TestMail
+ {
+ private const string FirstFrom = "Jennifer Parker";
+ private const string FirstSubject = "Photos from our trip";
+ private const string FirstBody = "Check out these awesome photos I took while in New Zealand!";
+
+ private const string SecondFrom = "Steve Bosniak";
+ private const string SecondSubject = "Build 2015 Dinner";
+ private const string SecondBody = "Want to go out for dinner after Build tonight?";
+
+ [TestCategory("EndToEnd/Mail")]
+ [TestMethod]
+ public void TestMailTile()
+ {
+ TileBinding small = new TileBinding()
+ {
+ Content = new TileBindingContentIconic()
+ {
+ Icon = new TileBasicImage() { Source = "Assets\\Mail.png" }
+ }
+ };
+
+ TileBinding medium = new TileBinding()
+ {
+ Branding = TileBranding.Logo,
+
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ GenerateFirstMessage(false),
+ GenerateSpacer(),
+ GenerateSecondMessage(false)
+ }
+ }
+ };
+
+ TileBinding wideAndLarge = new TileBinding()
+ {
+ Branding = TileBranding.NameAndLogo,
+
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ GenerateFirstMessage(true),
+ GenerateSpacer(),
+ GenerateSecondMessage(true)
+ }
+ }
+ };
+
+ TileContent content = new TileContent()
+ {
+ Visual = new TileVisual()
+ {
+ TileSmall = small,
+ TileMedium = medium,
+ TileWide = wideAndLarge,
+ TileLarge = wideAndLarge
+ }
+ };
+
+ string expectedXml = $@" ";
+
+ // Medium
+ expectedXml += @"";
+ expectedXml += GenerateXmlGroups(false);
+ expectedXml += " ";
+
+ // Wide
+ expectedXml += @"";
+ expectedXml += GenerateXmlGroups(true);
+ expectedXml += " ";
+
+ // Large
+ expectedXml += @"";
+ expectedXml += GenerateXmlGroups(true);
+ expectedXml += " ";
+
+ expectedXml += " ";
+
+ AssertHelper.AssertTile(expectedXml, content);
+ }
+
+ private static string GenerateXmlGroups(bool makeLarge)
+ {
+ return GenerateXmlGroup(FirstFrom, FirstSubject, FirstBody, makeLarge) + " " + GenerateXmlGroup(SecondFrom, SecondSubject, SecondBody, makeLarge);
+ }
+
+ private static string GenerateXmlGroup(string from, string subject, string body, bool makeLarge)
+ {
+ string xml = "{from} {subject} {body} ";
+
+ return xml;
+ }
+
+ private static AdaptiveText GenerateSpacer()
+ {
+ return new AdaptiveText();
+ }
+
+ private static AdaptiveGroup GenerateFirstMessage(bool makeLarge)
+ {
+ return GenerateMessage(FirstFrom, FirstSubject, FirstBody, makeLarge);
+ }
+
+ private static AdaptiveGroup GenerateSecondMessage(bool makeLarge)
+ {
+ return GenerateMessage(SecondFrom, SecondSubject, SecondBody, makeLarge);
+ }
+
+ private static AdaptiveGroup GenerateMessage(string from, string subject, string body, bool makeLarge)
+ {
+ return new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = from,
+ HintStyle = makeLarge ? AdaptiveTextStyle.Subtitle : AdaptiveTextStyle.Caption
+ },
+
+ new AdaptiveText()
+ {
+ Text = subject,
+ HintStyle = AdaptiveTextStyle.CaptionSubtle
+ },
+
+ new AdaptiveText()
+ {
+ Text = body,
+ HintStyle = AdaptiveTextStyle.CaptionSubtle
+ }
+ }
+ }
+ }
+ };
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/TestTileContentBuilder.cs b/components/Notifications/tests/TestTileContentBuilder.cs
new file mode 100644
index 000000000..576049b4a
--- /dev/null
+++ b/components/Notifications/tests/TestTileContentBuilder.cs
@@ -0,0 +1,115 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using System.Linq;
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class TestTileContentBuilder
+ {
+ [TestMethod]
+ public void AddTextTest_OnSmallTileOnly()
+ {
+ // Arrange
+ string text = "text on small tile";
+ TileContentBuilder builder = new TileContentBuilder();
+ builder.AddTile(TileSize.Small);
+
+ // Act
+ builder.AddText(text);
+
+ // Assert
+ var tileText = GetTileAdaptiveText(builder, TileSize.Small);
+ Assert.IsNotNull(tileText);
+ Assert.AreSame("text on small tile", (string)tileText.Text);
+ }
+
+ [TestMethod]
+ public void AddTextTest_OnMediumTileOnly()
+ {
+ // Arrange
+ string text = "text on medium tile";
+ TileContentBuilder builder = new TileContentBuilder();
+ builder.AddTile(TileSize.Medium);
+
+ // Act
+ builder.AddText(text);
+
+ // Assert
+ var tileText = GetTileAdaptiveText(builder, TileSize.Medium);
+ Assert.IsNotNull(tileText);
+ Assert.AreSame("text on medium tile", (string)tileText.Text);
+ }
+
+ [TestMethod]
+ public void AddTextTest_OnWideTileOnly()
+ {
+ // Arrange
+ string text = "text on wide tile";
+ TileContentBuilder builder = new TileContentBuilder();
+ builder.AddTile(TileSize.Wide);
+
+ // Act
+ builder.AddText(text);
+
+ // Assert
+ var tileText = GetTileAdaptiveText(builder, TileSize.Wide);
+ Assert.IsNotNull(tileText);
+ Assert.AreSame("text on wide tile", (string)tileText.Text);
+ }
+
+ [TestMethod]
+ public void AddTextTest_OnLargeTileOnly()
+ {
+ // Arrange
+ string text = "text on large tile";
+ TileContentBuilder builder = new TileContentBuilder();
+ builder.AddTile(TileSize.Large);
+
+ // Act
+ builder.AddText(text);
+
+ // Assert
+ var tileText = GetTileAdaptiveText(builder, TileSize.Large);
+ Assert.IsNotNull(tileText);
+ Assert.AreSame("text on large tile", (string)tileText.Text);
+ }
+
+ private static AdaptiveText GetTileAdaptiveText(TileContentBuilder builder, TileSize size)
+ {
+ TileBinding tileBinding;
+ switch (size)
+ {
+ case TileSize.Small:
+ tileBinding = builder.Content.Visual.TileSmall;
+ break;
+
+ case TileSize.Medium:
+ tileBinding = builder.Content.Visual.TileMedium;
+ break;
+
+ case TileSize.Wide:
+ tileBinding = builder.Content.Visual.TileWide;
+ break;
+
+ case TileSize.Large:
+ tileBinding = builder.Content.Visual.TileLarge;
+ break;
+
+ default:
+ return null;
+ }
+
+ var content = (TileBindingContentAdaptive)tileBinding.Content;
+ return content.Children.FirstOrDefault() as AdaptiveText;
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/TestToastArguments.cs b/components/Notifications/tests/TestToastArguments.cs
new file mode 100644
index 000000000..88f5cbd00
--- /dev/null
+++ b/components/Notifications/tests/TestToastArguments.cs
@@ -0,0 +1,502 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [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("-_.!~*'()", "-_.!~*'()");
+ }
+
+ [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");
+ }
+
+ [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.Add("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 TestAdd()
+ {
+ ToastArguments qs = new ToastArguments();
+
+ qs.Add("name", "Andrew");
+
+ AssertEqual(
+ new ToastArguments()
+ {
+ { "name", "Andrew" }
+ }, qs);
+
+ qs.Add("age", "22");
+
+ AssertEqual(
+ new ToastArguments()
+ {
+ { "name", "Andrew" },
+ { "age", "22" }
+ }, qs);
+
+ qs.Add("name", "Lei");
+
+ AssertEqual(
+ new ToastArguments()
+ {
+ { "name", "Lei" },
+ { "age", "22" }
+ }, qs);
+
+ string nullStr = null;
+ qs.Add("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()
+ .Add("isAdult", true)
+ .Add("isPremium", false)
+ .Add("age", 22)
+ .Add("level", 0)
+ .Add("gpa", 3.97)
+ .Add("percent", 97.3f);
+
+ args.Add("activationKind", ToastActivationType.Background);
+
+ AssertEqual(
+ new ToastArguments()
+ {
+ { "isAdult", "1" },
+ { "isPremium", "0" },
+ { "age", "22" },
+ { "level", "0" },
+ { "gpa", "3.97" },
+ { "percent", "97.3" },
+ { "activationKind", "1" }
+ }, 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"));
+
+ 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));
+
+ // 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"));
+
+ 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));
+ }
+
+ private static void AssertIndexer(string expected, string queryString, string paramName)
+ {
+ ToastArguments q = ToastArguments.Parse(queryString);
+
+ Assert.AreEqual(expected, q[paramName]);
+ }
+
+ 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;
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/TestToastContentBuilder.cs b/components/Notifications/tests/TestToastContentBuilder.cs
new file mode 100644
index 000000000..1ea03b124
--- /dev/null
+++ b/components/Notifications/tests/TestToastContentBuilder.cs
@@ -0,0 +1,1201 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using System;
+using System.Linq;
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class TestToastContentBuilder
+ {
+ [TestMethod]
+ public void AddCustomTimeStampTest_WithCustomTimeStamp_ReturnSelfWithCustomTimeStampAdded()
+ {
+ // Arrange
+ DateTime testCustomTimeStamp = DateTime.UtcNow;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddCustomTimeStamp(testCustomTimeStamp);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testCustomTimeStamp, builder.Content.DisplayTimestamp);
+ }
+
+ [TestMethod]
+ public void AddHeaderTest_WithExpectedArgs_ReturnSelfWithHeaderAdded()
+ {
+ // Arrange
+ string testToastHeaderId = "Test Header ID";
+ string testToastTitle = "Test Toast Title";
+ string testToastArguments = "Test Toast Arguments";
+
+ // 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, 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()
+ {
+ // Arrange
+ string testToastLaunchArugments = "Test Toast Launch Args";
+ ToastActivationType testToastActivationType = ToastActivationType.Background;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddToastActivationInfo(testToastLaunchArugments, testToastActivationType);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testToastLaunchArugments, builder.Content.Launch);
+ Assert.AreEqual(testToastActivationType, builder.Content.ActivationType);
+ }
+
+ [TestMethod]
+ public void AddArgumentTest_Basic_ReturnSelfWithArgumentsAdded()
+ {
+ // 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
+ .AddArgument("isPurelyInformational");
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual("isPurelyInformational", builder.Content.Launch);
+ }
+
+ [TestMethod]
+ 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
+ const string userIdKey = "userId";
+ const int userIdValue = 542;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder
+ .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(new ToastButton()
+ .SetContent("Decline")
+ .AddArgument("action", "decline")
+ .SetBackgroundActivation())
+ .AddButton(new ToastButton()
+ .SetContent("Report")
+ .SetProtocolActivation(new Uri("https://microsoft.com")));
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+
+ // 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(new ToastButton()
+ .SetContent("Accept")
+ .AddArgument("action", "accept")
+ .AddArgument("userId", 601)
+ .SetBackgroundActivation())
+
+ // Add generic arguments halfway through (in this case shouldn't overwrite anything)
+ .AddArgument("userId", 542)
+
+ .AddButton(new ToastButton()
+ .SetContent("Decline")
+ .AddArgument("action", "decline")
+ .AddArgument("userId", 601)
+ .SetBackgroundActivation());
+
+ // 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(new ToastButton()
+ .SetContent("View")
+ .AddArgument("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(new ToastButton()
+ .SetContent("Accept")
+ .AddArgument("action", "accept")
+ .SetBackgroundActivation())
+
+ .AddArgument("userId", 542)
+
+ .SetProtocolActivation(new Uri("https://msn.com/"))
+
+ .AddArgument("name", "Andrew")
+
+ .AddButton(new ToastButton()
+ .SetContent("Decline")
+ .AddArgument("action", "decline")
+ .SetBackgroundActivation());
+
+ // 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]
+ 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_InvalidDismissAfterArguments_ReturnSelf()
+ {
+ new ToastButton()
+ .SetContent("View")
+ .AddArgument("action", "view")
+ .SetDismissActivation();
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ToastButtonBuilders_InvalidSnoozeAfterArguments_ReturnSelf()
+ {
+ new ToastButton()
+ .SetContent("View")
+ .AddArgument("action", "view")
+ .SetSnoozeActivation();
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ToastButtonBuilders_InvalidSnoozeWithIdAfterArguments_ReturnSelf()
+ {
+ new ToastButton()
+ .SetContent("View")
+ .AddArgument("action", "view")
+ .SetSnoozeActivation("snoozeId");
+ }
+
+ [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]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ToastButtonBuilders_InvalidArgumentsAfterSnooze_ReturnSelf()
+ {
+ new ToastButton()
+ .SetContent("Later")
+ .SetSnoozeActivation()
+ .AddArgument("action", "later");
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ToastButtonBuilders_InvalidArgumentsAfterSnoozeWithId_ReturnSelf()
+ {
+ new ToastButton()
+ .SetContent("Later")
+ .SetSnoozeActivation("myId")
+ .AddArgument("action", "later");
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void ToastButtonBuilders_InvalidArgumentsAfterDismissActivation_ReturnSelf()
+ {
+ new ToastButton()
+ .SetContent("Later")
+ .SetDismissActivation()
+ .AddArgument("action", "later");
+ }
+
+ [TestMethod]
+ public void SetToastDurationTest_WithCustomToastDuration_ReturnSelfWithCustomToastDurationSet()
+ {
+ // Arrange
+ ToastDuration testToastDuration = ToastDuration.Long;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.SetToastDuration(testToastDuration);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testToastDuration, builder.Content.Duration);
+ }
+
+ [TestMethod]
+ public void SetToastScenarioTest_WithCustomToastScenario_ReturnSelfWithCustomToastScenarioSet()
+ {
+ // Arrange
+ ToastScenario testToastScenario = ToastScenario.Default;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.SetToastScenario(testToastScenario);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testToastScenario, builder.Content.Scenario);
+ }
+
+ [TestMethod]
+ public void AddAudioTest_WithAudioUriOnly_ReturnSelfWithCustomAudioAdded()
+ {
+ // Arrange
+ Uri testAudioUriSrc = new Uri("C:/justatesturi.mp3");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString);
+ }
+
+ [TestMethod]
+ public void AddAudioTest_WithFullArgs_ReturnSelfWithCustomAudioAddedWithAllOptionsSet()
+ {
+ // Arrange
+ Uri testAudioUriSrc = new Uri("C:/justatesturi.mp3");
+ bool testToastAudioLoop = true;
+ bool testToastAudioSilent = true;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc, testToastAudioLoop, testToastAudioSilent);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString);
+ Assert.AreEqual(testToastAudioLoop, builder.Content.Audio.Loop);
+ Assert.AreEqual(testToastAudioSilent, builder.Content.Audio.Silent);
+ }
+
+ [TestMethod]
+ public void AddAudioTest_WithMsWinSoundEvent_ReturnSelfWithCustomAudioAdded()
+ {
+ // Arrange
+ Uri testAudioUriSrc = new Uri("ms-winsoundevent:Notification.Reminder");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString);
+ }
+
+ [TestMethod]
+ public void AddAudioTest_WithMsAppx_ReturnSelfWithCustomAudioAdded()
+ {
+ // Arrange
+ Uri testAudioUriSrc = new Uri("ms-appx:///Assets/Audio.mp3");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAudio(testAudioUriSrc);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAudioUriSrc.OriginalString, builder.Content.Audio.Src.OriginalString);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void AddAudioTest_WithInvalidMsUri_ThrowException()
+ {
+ // Arrange
+ Uri testAudioUriSrc = new Uri("ms-doesntexist:Notification.Reminder");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ builder.AddAudio(testAudioUriSrc);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void AddAudioTest_WithInvalidAppDataUri_ThrowException()
+ {
+ // Arrange (ms-appdata isn't currently supported)
+ Uri testAudioUriSrc = new Uri("ms-appdata:///local/Sound.mp3");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ builder.AddAudio(testAudioUriSrc);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void AddAudioTest_WithInvalidHttpUri_ThrowException()
+ {
+ // Arrange
+ Uri testAudioUriSrc = new Uri("https://myaudio.com/song.mp3");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ builder.AddAudio(testAudioUriSrc);
+ }
+
+ [TestMethod]
+ public void AddAudioTest_WithAudioObject_ReturnSelfWithCustomAudioAdded()
+ {
+ // Arrange
+ var audio = new ToastAudio()
+ {
+ Silent = true
+ };
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAudio(audio);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreSame(audio, builder.Content.Audio);
+ }
+
+ [TestMethod]
+ public void AddAttributionTextTest_WithSimpleText_ReturnSelfWithCustomAttributionTextAdded()
+ {
+ // Arrange
+ string testAttributionText = "Test Attribution Text";
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAttributionText(testAttributionText);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAttributionText, builder.Content.Visual.BindingGeneric.Attribution.Text);
+ }
+
+ [TestMethod]
+ public void AddAttributionTextTest_WithTextAndLanguage_ReturnSelfWithCustomAttributionTextAndLanguageAdded()
+ {
+ // Arrange
+ string testAttributionText = "Test Attribution Text";
+ string testAttributionTextLanguage = "en-US";
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAttributionText(testAttributionText, testAttributionTextLanguage);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAttributionText, builder.Content.Visual.BindingGeneric.Attribution.Text);
+ Assert.AreEqual(testAttributionTextLanguage, builder.Content.Visual.BindingGeneric.Attribution.Language);
+ }
+
+ [TestMethod]
+ public void AddAppLogoOverrideTest_WithLogoUriOnly_ReturnSelfWithCustomLogoAdded()
+ {
+ // Arrange
+ Uri testAppLogoUriSrc = new Uri("C:/justatesturi.jpg");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAppLogoOverride(testAppLogoUriSrc);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAppLogoUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.AppLogoOverride.Source);
+ }
+
+ [TestMethod]
+ public void AddAppLogoOverrideTest_WithCustomLogoAndFullOptions_ReturnSelfWithCustomLogoAndOptionsAdded()
+ {
+ // Arrange
+ Uri testAppLogoUriSrc = new Uri("C:/justatesturi.jpg");
+ ToastGenericAppLogoCrop testCropOption = ToastGenericAppLogoCrop.Circle;
+ string testLogoAltText = "Test Logo Alt Text";
+ bool testLogoAddImageQuery = true;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddAppLogoOverride(testAppLogoUriSrc, testCropOption, testLogoAltText, testLogoAddImageQuery);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testAppLogoUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.AppLogoOverride.Source);
+ Assert.AreEqual(testCropOption, builder.Content.Visual.BindingGeneric.AppLogoOverride.HintCrop);
+ Assert.AreEqual(testLogoAltText, builder.Content.Visual.BindingGeneric.AppLogoOverride.AlternateText);
+ Assert.AreEqual(testLogoAddImageQuery, builder.Content.Visual.BindingGeneric.AppLogoOverride.AddImageQuery);
+ }
+
+ [TestMethod]
+ public void AddHeroImageTest_WithHeroImageUriOnly_ReturnSelfWithHeroImageAdded()
+ {
+ // Arrange
+ Uri testHeroImageUriSrc = new Uri("C:/justatesturi.jpg");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddHeroImage(testHeroImageUriSrc);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testHeroImageUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.HeroImage.Source);
+ }
+
+ [TestMethod]
+ public void AddHeroImageTest_WithHeroImageUriAndFullOptions_ReturnSelfWithHeroImageAndOptionsAdded()
+ {
+ // Arrange
+ Uri testHeroImageUriSrc = new Uri("C:/justatesturi.jpg");
+ string testHeroImageAltText = "Test Hero Image Text";
+ bool testHeroImageAddImageQuery = true;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddHeroImage(testHeroImageUriSrc, testHeroImageAltText, testHeroImageAddImageQuery);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testHeroImageUriSrc.OriginalString, builder.Content.Visual.BindingGeneric.HeroImage.Source);
+ Assert.AreEqual(testHeroImageAltText, builder.Content.Visual.BindingGeneric.HeroImage.AlternateText);
+ Assert.AreEqual(testHeroImageAddImageQuery, builder.Content.Visual.BindingGeneric.HeroImage.AddImageQuery);
+ }
+
+ [TestMethod]
+ public void AddInlineImageTest_WithInlineImageUriOnly_ReturnSelfWithInlineImageAdded()
+ {
+ // Arrange
+ Uri testInlineImageUriSrc = new Uri("C:/justatesturi.jpg");
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddInlineImage(testInlineImageUriSrc);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.AreEqual(testInlineImageUriSrc.OriginalString, (builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveImage).Source);
+ }
+
+ [TestMethod]
+ public void AddInlineImageTest_WithInlineImageAndFullOptions_ReturnSelfWithInlineImageAndOptionsAdded()
+ {
+ // Arrange
+ Uri testInlineImageUriSrc = new Uri("C:/justatesturi.jpg");
+ string testInlineImageAltText = "Test Inline Image Text";
+ bool testInlineImageAddImageQuery = true;
+
+ // Act
+ ToastContentBuilder builder = new ToastContentBuilder();
+ ToastContentBuilder anotherReference = builder.AddInlineImage(testInlineImageUriSrc, testInlineImageAltText, testInlineImageAddImageQuery);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+
+ var image = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveImage;
+
+ Assert.AreEqual(testInlineImageUriSrc.OriginalString, image.Source);
+ Assert.AreEqual(testInlineImageAltText, image.AlternateText);
+ Assert.AreEqual(testInlineImageAddImageQuery, image.AddImageQuery);
+ }
+
+ [TestMethod]
+ public void AddProgressBarTest_WithoutInputArgs_ReturnSelfWithNonIndeterminateBindableProgressBarAdded()
+ {
+ // Arrange
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddProgressBar();
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var progressBar = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveProgressBar;
+
+ Assert.IsNotNull(progressBar.Title.BindingName);
+ Assert.IsNotNull(progressBar.Value.BindingName);
+ Assert.IsNotNull(progressBar.ValueStringOverride.BindingName);
+ Assert.IsNotNull(progressBar.Status.BindingName);
+ }
+
+ [TestMethod]
+ public void AddProgressBarTest_WithFixedPropertiesAndDeterminateValue_ReturnSelfWithFixedValueAndPropertiesProgressBarAdded()
+ {
+ // Arrange
+ string testProgressBarTitle = "Test Progress Bar Title";
+ double testProgressBarValue = 0.25;
+ string testValueStringOverride = "Test Value String Override";
+ string testProgressBarStatus = "Test Progress Bar Status";
+
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddProgressBar(testProgressBarTitle, testProgressBarValue, false, testValueStringOverride, testProgressBarStatus);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var progressBar = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveProgressBar;
+
+ Assert.IsNull(progressBar.Title.BindingName);
+ Assert.AreEqual(testProgressBarTitle, (string)progressBar.Title);
+
+ Assert.IsNull(progressBar.Value.BindingName);
+ Assert.AreEqual(testProgressBarValue, ((AdaptiveProgressBarValue)progressBar.Value).Value);
+
+ Assert.IsNull(progressBar.ValueStringOverride.BindingName);
+ Assert.AreEqual(testValueStringOverride, (string)progressBar.ValueStringOverride);
+
+ Assert.IsNull(progressBar.Status.BindingName);
+ Assert.AreEqual(testProgressBarStatus, (string)progressBar.Status);
+ }
+
+ [TestMethod]
+ public void AddProgressBarTest_WithIndeterminateValue_ReturnSelfWithIndeterminateProgressBarAdded()
+ {
+ // Arrange
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddProgressBar(isIndeterminate: true);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var progressBar = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveProgressBar;
+
+ Assert.IsTrue(((AdaptiveProgressBarValue)progressBar.Value).IsIndeterminate);
+ }
+
+ [TestMethod]
+ public void AddTextTest_WithSimpleText_ReturnSelfWithTextAdded()
+ {
+ // Arrange
+ string testText = "Test Text";
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddText(testText);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var text = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveText;
+
+ Assert.AreEqual(testText, (string)text.Text);
+ }
+
+ [TestMethod]
+ public void AddTextTest_WithMultipleTexts_ReturnSelfWithAllTextsAdded()
+ {
+ // Arrange
+ string testText1 = "Test Header";
+ string testText2 = "Test Text";
+ string testText3 = "Test Text Again";
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddText(testText1)
+ .AddText(testText2)
+ .AddText(testText3);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var texts = builder.Content.Visual.BindingGeneric.Children.Take(3).Cast().ToList();
+
+ Assert.AreEqual(testText1, (string)texts[0].Text);
+ Assert.AreEqual(testText2, (string)texts[1].Text);
+ Assert.AreEqual(testText3, (string)texts[2].Text);
+ }
+
+ [TestMethod]
+ public void AddTextTest_WithTextAndFullOptions_ReturnSelfWithTextAndAllOptionsAdded()
+ {
+ // Arrange
+ string testText = "Test Text";
+ AdaptiveTextStyle testStyle = AdaptiveTextStyle.Header;
+ bool testWrapHint = true;
+ int testHintMaxLine = 2;
+ int testHintMinLine = 1;
+ AdaptiveTextAlign testAlign = AdaptiveTextAlign.Auto;
+ string testLanguage = "en-US";
+
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddText(testText, testStyle, testWrapHint, testHintMaxLine, testHintMinLine, testAlign, testLanguage);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var text = builder.Content.Visual.BindingGeneric.Children.First() as AdaptiveText;
+
+ Assert.AreEqual(testText, (string)text.Text);
+ Assert.AreEqual(testHintMaxLine, text.HintMaxLines);
+ Assert.AreEqual(testLanguage, text.Language);
+
+ // These values should still be the default values, since they aren't used for top-level text
+ Assert.AreEqual(AdaptiveTextStyle.Default, text.HintStyle);
+ Assert.IsNull(text.HintWrap);
+ Assert.IsNull(text.HintMinLines);
+ Assert.AreEqual(AdaptiveTextAlign.Default, text.HintAlign);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(InvalidOperationException))]
+ public void AddTextTest_WithMoreThan4LinesOfText_ThrowInvalidOperationException()
+ {
+ // Arrange
+ string testText1 = "Test Header";
+ string testText2 = "Test Text";
+ string testText3 = "Test Text Again";
+ string testText4 = "Test Text Again x2";
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ _ = builder.AddText(testText1)
+ .AddText(testText2)
+ .AddText(testText3)
+ .AddText(testText4);
+ }
+
+ [TestMethod]
+ [ExpectedException(typeof(ArgumentOutOfRangeException))]
+ public void AddTextTest_WithMaxLinesValueLargerThan2_ThrowArgumentOutOfRangeException()
+ {
+ // Arrange
+ string testText1 = "Test Header";
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ _ = builder.AddText(testText1, hintMaxLines: 3);
+ }
+
+ [TestMethod]
+ public void AddVisualChildTest_WithCustomVisual_ReturnSelfWithCustomVisualAdded()
+ {
+ // Arrange
+ // Taken from : https://docs.microsoft.com/en-us/windows/uwp/design/shell/tiles-and-notifications/adaptive-interactive-toasts#adaptive-content
+ AdaptiveGroup group = new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "52 attendees",
+ HintStyle = AdaptiveTextStyle.Base
+ },
+ new AdaptiveText()
+ {
+ Text = "23 minute drive",
+ HintStyle = AdaptiveTextStyle.CaptionSubtle
+ }
+ }
+ },
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "1 Microsoft Way",
+ HintStyle = AdaptiveTextStyle.CaptionSubtle,
+ HintAlign = AdaptiveTextAlign.Right
+ },
+ new AdaptiveText()
+ {
+ Text = "Bellevue, WA 98008",
+ HintStyle = AdaptiveTextStyle.CaptionSubtle,
+ HintAlign = AdaptiveTextAlign.Right
+ }
+ }
+ }
+ }
+ };
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddVisualChild(group);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ Assert.IsInstanceOfType(builder.Content.Visual.BindingGeneric.Children.First(), typeof(AdaptiveGroup));
+ }
+
+ [TestMethod]
+ public void AddButtonTest_WithTextOnlyButton_ReturnSelfWithButtonAdded()
+ {
+ // Arrange
+ string testButtonContent = "Test Button Content";
+ ToastActivationType testToastActivationType = ToastActivationType.Background;
+ string testButtonLaunchArgs = "Test Launch Args";
+ 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, button.Arguments);
+ }
+
+ [TestMethod]
+ public void AddButtonTest_WithCustomImageAndTextButton_ReturnSelfWithButtonAdded()
+ {
+ // Arrange
+ string testButtonContent = "Test Button Content";
+ ToastActivationType testToastActivationType = ToastActivationType.Background;
+ string testButtonLaunchArgs = "Test Launch Args";
+ 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, button.Arguments);
+ Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri);
+ }
+
+ [TestMethod]
+ public void AddButtonTest_WithTextBoxId_ReturnSelfWithButtonAdded()
+ {
+ // Arrange
+ string testInputTextBoxId = Guid.NewGuid().ToString();
+ string testButtonContent = "Test Button Content";
+ ToastActivationType testToastActivationType = ToastActivationType.Background;
+ string testButtonLaunchArgs = "Test Launch Args";
+ 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, button.Arguments);
+ Assert.AreEqual(testImageUriSrc.OriginalString, button.ImageUri);
+ }
+
+ [TestMethod]
+ public void AddInputTextBoxTest_WithStringIdOnly_ReturnSelfWithInputTextBoxAdded()
+ {
+ // Arrange
+ string testInputTextBoxId = Guid.NewGuid().ToString();
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddInputTextBox(testInputTextBoxId);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var inputTextBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastTextBox;
+
+ Assert.AreEqual(testInputTextBoxId, inputTextBox.Id);
+ }
+
+ [TestMethod]
+ public void AddInputTextBoxTest_WithPlaceHolderContentAndTitle_ReturnSelfWithInputTextBoxAndAllOptionsAdded()
+ {
+ // Arrange
+ string testInputTextBoxId = Guid.NewGuid().ToString();
+ string testInputTextBoxPlaceHolderContent = "Placeholder Content";
+ string testInputTextBoxTitle = "Test Title";
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddInputTextBox(testInputTextBoxId, testInputTextBoxPlaceHolderContent, testInputTextBoxTitle);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var inputTextBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastTextBox;
+
+ Assert.AreEqual(testInputTextBoxId, inputTextBox.Id);
+ Assert.AreEqual(testInputTextBoxPlaceHolderContent, inputTextBox.PlaceholderContent);
+ Assert.AreEqual(testInputTextBoxTitle, inputTextBox.Title);
+ }
+
+ [TestMethod]
+ public void AddComboBoxTest_WithMultipleChoices_ReturnSelfWithComboBoxAndAllChoicesAdded()
+ {
+ // Arrange
+ string testComboBoxId = Guid.NewGuid().ToString();
+ var choice1 = (Guid.NewGuid().ToString(), "Test Choice 1");
+ var choice2 = (Guid.NewGuid().ToString(), "Test Choice 2");
+ var choice3 = (Guid.NewGuid().ToString(), "Test Choice 3");
+
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddComboBox(testComboBoxId, choice1, choice2, choice3);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var comboBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastSelectionBox;
+
+ Assert.AreEqual(testComboBoxId, comboBox.Id);
+ Assert.AreEqual(choice1.Item1, comboBox.Items[0].Id);
+ Assert.AreEqual(choice2.Item1, comboBox.Items[1].Id);
+ Assert.AreEqual(choice3.Item1, comboBox.Items[2].Id);
+
+ Assert.AreEqual(choice1.Item2, comboBox.Items[0].Content);
+ Assert.AreEqual(choice2.Item2, comboBox.Items[1].Content);
+ Assert.AreEqual(choice3.Item2, comboBox.Items[2].Content);
+ }
+
+ [TestMethod]
+ public void AddComboBoxTest_WithMultipleChoicesAndDefaultSelected_ReturnSelfWithComboBoxAddedWithAllChoicesAndDefaultSelection()
+ {
+ // Arrange
+ string testComboBoxId = Guid.NewGuid().ToString();
+ var choice1 = (Guid.NewGuid().ToString(), "Test Choice 1");
+ var choice2 = (Guid.NewGuid().ToString(), "Test Choice 2");
+ var choice3 = (Guid.NewGuid().ToString(), "Test Choice 3");
+ string defaultChoice = choice2.Item1;
+
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddComboBox(testComboBoxId, defaultChoice, choice1, choice2, choice3);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var comboBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastSelectionBox;
+
+ Assert.AreEqual(testComboBoxId, comboBox.Id);
+ Assert.AreEqual(choice1.Item1, comboBox.Items[0].Id);
+ Assert.AreEqual(choice2.Item1, comboBox.Items[1].Id);
+ Assert.AreEqual(choice3.Item1, comboBox.Items[2].Id);
+
+ Assert.AreEqual(choice1.Item2, comboBox.Items[0].Content);
+ Assert.AreEqual(choice2.Item2, comboBox.Items[1].Content);
+ Assert.AreEqual(choice3.Item2, comboBox.Items[2].Content);
+
+ Assert.AreEqual(defaultChoice, comboBox.DefaultSelectionBoxItemId);
+ }
+
+ [TestMethod]
+ public void AddComboBoxTest_WithMultipleChoiceAndDefaultSelectedAndTitle_ReturnSelfWithComboBoxAddedWithAllChoicesAndDefaultSelectionAndTitle()
+ {
+ // Arrange
+ string testComboBoxId = Guid.NewGuid().ToString();
+ var choice1 = (Guid.NewGuid().ToString(), "Test Choice 1");
+ var choice2 = (Guid.NewGuid().ToString(), "Test Choice 2");
+ var choice3 = (Guid.NewGuid().ToString(), "Test Choice 3");
+ string defaultChoice = choice2.Item1;
+ string testComboBoxTitle = "Test Title";
+
+ ToastContentBuilder builder = new ToastContentBuilder();
+
+ // Act
+ ToastContentBuilder anotherReference = builder.AddComboBox(testComboBoxId, testComboBoxTitle, defaultChoice, choice1, choice2, choice3);
+
+ // Assert
+ Assert.AreSame(builder, anotherReference);
+ var comboBox = (builder.Content.Actions as ToastActionsCustom).Inputs.First() as ToastSelectionBox;
+
+ Assert.AreEqual(testComboBoxId, comboBox.Id);
+ Assert.AreEqual(choice1.Item1, comboBox.Items[0].Id);
+ Assert.AreEqual(choice2.Item1, comboBox.Items[1].Id);
+ Assert.AreEqual(choice3.Item1, comboBox.Items[2].Id);
+
+ Assert.AreEqual(choice1.Item2, comboBox.Items[0].Content);
+ Assert.AreEqual(choice2.Item2, comboBox.Items[1].Content);
+ Assert.AreEqual(choice3.Item2, comboBox.Items[2].Content);
+
+ Assert.AreEqual(defaultChoice, comboBox.DefaultSelectionBoxItemId);
+ Assert.AreEqual(testComboBoxTitle, comboBox.Title);
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/TestWeather.cs b/components/Notifications/tests/TestWeather.cs
new file mode 100644
index 000000000..a171dfabb
--- /dev/null
+++ b/components/Notifications/tests/TestWeather.cs
@@ -0,0 +1,295 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class TestWeather
+ {
+ private const string ImageMostlyCloudy = "Assets\\Tiles\\Mostly Cloudy.png";
+ private const string ImageSunny = "Assets\\Tiles\\Sunny.png";
+ private const string ImageCloudy = "Assets\\Tiles\\Cloudy.png";
+
+ private const string BackgroundImageMostlyCloudy = "Assets\\Tiles\\Mostly Cloudy-Background.jpg";
+
+ [TestMethod]
+ public void TestWeatherTile()
+ {
+ var backgroundImage = BackgroundImageMostlyCloudy;
+ int overlay = 30;
+
+ TileBindingContentAdaptive smallContent = new TileBindingContentAdaptive()
+ {
+ TextStacking = TileTextStacking.Center,
+ BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay },
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "Mon",
+ HintStyle = AdaptiveTextStyle.Body,
+ HintAlign = AdaptiveTextAlign.Center
+ },
+
+ new AdaptiveText()
+ {
+ Text = "63°",
+ HintStyle = AdaptiveTextStyle.Base,
+ HintAlign = AdaptiveTextAlign.Center
+ }
+ }
+ };
+
+ TileBindingContentAdaptive mediumContent = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay },
+ Children =
+ {
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ GenerateMediumSubgroup("Mon", ImageMostlyCloudy, 63, 42),
+ GenerateMediumSubgroup("Tue", ImageCloudy, 57, 38)
+ }
+ }
+ }
+ };
+
+ TileBindingContentAdaptive wideContent = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay },
+ Children =
+ {
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ GenerateWideSubgroup("Mon", ImageMostlyCloudy, 63, 42),
+ GenerateWideSubgroup("Tue", ImageCloudy, 57, 38),
+ GenerateWideSubgroup("Wed", ImageSunny, 59, 43),
+ GenerateWideSubgroup("Thu", ImageSunny, 62, 42),
+ GenerateWideSubgroup("Fri", ImageSunny, 71, 66)
+ }
+ }
+ }
+ };
+
+ TileBindingContentAdaptive largeContent = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage() { Source = backgroundImage, HintOverlay = overlay },
+ Children =
+ {
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ HintWeight = 30,
+ Children =
+ {
+ new AdaptiveImage() { Source = ImageMostlyCloudy }
+ }
+ },
+
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "Monday",
+ HintStyle = AdaptiveTextStyle.Base
+ },
+
+ new AdaptiveText()
+ {
+ Text = "63° / 42°"
+ },
+
+ new AdaptiveText()
+ {
+ Text = "20% chance of rain",
+ HintStyle = AdaptiveTextStyle.CaptionSubtle
+ },
+
+ new AdaptiveText()
+ {
+ Text = "Winds 5 mph NE",
+ HintStyle = AdaptiveTextStyle.CaptionSubtle
+ }
+ }
+ }
+ }
+ },
+
+ // For spacing
+ new AdaptiveText(),
+
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ GenerateLargeSubgroup("Tue", ImageCloudy, 57, 38),
+ GenerateLargeSubgroup("Wed", ImageSunny, 59, 43),
+ GenerateLargeSubgroup("Thu", ImageSunny, 62, 42),
+ GenerateLargeSubgroup("Fri", ImageSunny, 71, 66)
+ }
+ }
+ }
+ };
+
+ TileContent content = new TileContent()
+ {
+ Visual = new TileVisual()
+ {
+ DisplayName = "Seattle",
+
+ TileSmall = new TileBinding()
+ {
+ Content = smallContent
+ },
+
+ TileMedium = new TileBinding()
+ {
+ Content = mediumContent,
+ Branding = TileBranding.Name
+ },
+
+ TileWide = new TileBinding()
+ {
+ Content = wideContent,
+ Branding = TileBranding.NameAndLogo
+ },
+
+ TileLarge = new TileBinding()
+ {
+ Content = largeContent,
+ Branding = TileBranding.NameAndLogo
+ }
+ }
+ };
+
+ string expectedPayload = $@"{GenerateStringBackgroundImage()}Mon 63° {GenerateStringBackgroundImage()}";
+
+ // Medium tile subgroups
+ expectedPayload += GenerateStringMediumSubgroup("Mon", ImageMostlyCloudy, 63, 42);
+ expectedPayload += GenerateStringMediumSubgroup("Tue", ImageCloudy, 57, 38);
+
+ expectedPayload += " ";
+
+ // Wide tile
+ expectedPayload += @"";
+ expectedPayload += GenerateStringBackgroundImage();
+ expectedPayload += "";
+
+ // Wide tile subgroups
+ expectedPayload += GenerateStringWideSubgroup("Mon", ImageMostlyCloudy, 63, 42);
+ expectedPayload += GenerateStringWideSubgroup("Tue", ImageCloudy, 57, 38);
+ expectedPayload += GenerateStringWideSubgroup("Wed", ImageSunny, 59, 43);
+ expectedPayload += GenerateStringWideSubgroup("Thu", ImageSunny, 62, 42);
+ expectedPayload += GenerateStringWideSubgroup("Fri", ImageSunny, 71, 66);
+
+ expectedPayload += " ";
+
+ // Large tile
+ expectedPayload += @"";
+ expectedPayload += GenerateStringBackgroundImage();
+ expectedPayload += $@"Monday 63° / 42° 20% chance of rain Winds 5 mph NE ";
+
+ expectedPayload += " ";
+ expectedPayload += "";
+
+ // Large tile subgroups
+ expectedPayload += GenerateStringLargeSubgroup("Tue", ImageCloudy, 57, 38);
+ expectedPayload += GenerateStringLargeSubgroup("Wed", ImageSunny, 59, 43);
+ expectedPayload += GenerateStringLargeSubgroup("Thu", ImageSunny, 62, 42);
+ expectedPayload += GenerateStringLargeSubgroup("Fri", ImageSunny, 71, 66);
+
+ expectedPayload += " ";
+
+ AssertHelper.AssertTile(expectedPayload, content);
+ }
+
+ private static string GenerateStringBackgroundImage()
+ {
+ return $@" ";
+ }
+
+ private static string GenerateStringMediumSubgroup(string day, string image, int high, int low)
+ {
+ return $@"{day} {high}° {low}° ";
+ }
+
+ private static string GenerateStringWideSubgroup(string day, string image, int high, int low)
+ {
+ return $@"{day} {high}° {low}° ";
+ }
+
+ private static string GenerateStringLargeSubgroup(string day, string image, int high, int low)
+ {
+ return $@"{day} {high}° {low}° ";
+ }
+
+ private static AdaptiveSubgroup GenerateMediumSubgroup(string day, string image, int high, int low)
+ {
+ return new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = day,
+ HintAlign = AdaptiveTextAlign.Center
+ },
+
+ new AdaptiveImage()
+ {
+ Source = image,
+ HintRemoveMargin = true
+ },
+
+ new AdaptiveText()
+ {
+ Text = high + "°",
+ HintAlign = AdaptiveTextAlign.Center
+ },
+
+ new AdaptiveText()
+ {
+ Text = low + "°",
+ HintAlign = AdaptiveTextAlign.Center,
+ HintStyle = AdaptiveTextStyle.CaptionSubtle
+ }
+ }
+ };
+ }
+
+ private static AdaptiveSubgroup GenerateWideSubgroup(string day, string image, int high, int low)
+ {
+ var subgroup = GenerateMediumSubgroup(day, image, high, low);
+
+ subgroup.HintWeight = 1;
+
+ return subgroup;
+ }
+
+ private static AdaptiveSubgroup GenerateLargeSubgroup(string day, string image, int high, int low)
+ {
+ var subgroup = GenerateWideSubgroup(day, image, high, low);
+
+ (subgroup.Children[1] as AdaptiveImage).HintRemoveMargin = null;
+
+ return subgroup;
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/Test_Adaptive_Xml.cs b/components/Notifications/tests/Test_Adaptive_Xml.cs
new file mode 100644
index 000000000..bbf50d7cf
--- /dev/null
+++ b/components/Notifications/tests/Test_Adaptive_Xml.cs
@@ -0,0 +1,600 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using System;
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class Test_Adaptive_Xml
+ {
+ [TestMethod]
+ public void Test_Adaptive_Text_Defaults()
+ {
+ AssertAdaptiveChild(" ", new AdaptiveText());
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_Text()
+ {
+ AssertAdaptiveChild("Hello & Goodbye ", new AdaptiveText()
+ {
+ Text = "Hello & Goodbye"
+ });
+
+ // Data binding should work
+ AssertAdaptiveChild("{title} ", new AdaptiveText()
+ {
+ Text = new BindableString("title")
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintStyle_Values()
+ {
+ AssertAdaptiveTextStyle("caption", AdaptiveTextStyle.Caption);
+ AssertAdaptiveTextStyle("captionSubtle", AdaptiveTextStyle.CaptionSubtle);
+ AssertAdaptiveTextStyle("base", AdaptiveTextStyle.Base);
+ AssertAdaptiveTextStyle("baseSubtle", AdaptiveTextStyle.BaseSubtle);
+ AssertAdaptiveTextStyle("body", AdaptiveTextStyle.Body);
+ AssertAdaptiveTextStyle("bodySubtle", AdaptiveTextStyle.BodySubtle);
+ AssertAdaptiveTextStyle("subtitle", AdaptiveTextStyle.Subtitle);
+ AssertAdaptiveTextStyle("subtitleSubtle", AdaptiveTextStyle.SubtitleSubtle);
+ AssertAdaptiveTextStyle("title", AdaptiveTextStyle.Title);
+ AssertAdaptiveTextStyle("titleSubtle", AdaptiveTextStyle.TitleSubtle);
+ AssertAdaptiveTextStyle("titleNumeral", AdaptiveTextStyle.TitleNumeral);
+ AssertAdaptiveTextStyle("subheader", AdaptiveTextStyle.Subheader);
+ AssertAdaptiveTextStyle("subheaderSubtle", AdaptiveTextStyle.SubheaderSubtle);
+ AssertAdaptiveTextStyle("subheaderNumeral", AdaptiveTextStyle.SubheaderNumeral);
+ AssertAdaptiveTextStyle("header", AdaptiveTextStyle.Header);
+ AssertAdaptiveTextStyle("headerSubtle", AdaptiveTextStyle.HeaderSubtle);
+ AssertAdaptiveTextStyle("headerNumeral", AdaptiveTextStyle.HeaderNumeral);
+ }
+
+ private static void AssertAdaptiveTextStyle(string expectedPropertyValue, AdaptiveTextStyle style)
+ {
+ AssertAdaptiveTextPropertyValue("hint-style", expectedPropertyValue, new AdaptiveText()
+ {
+ HintStyle = style
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintAlign_Values()
+ {
+ AssertAdaptiveTextAlign("auto", AdaptiveTextAlign.Auto);
+ AssertAdaptiveTextAlign("left", AdaptiveTextAlign.Left);
+ AssertAdaptiveTextAlign("center", AdaptiveTextAlign.Center);
+ AssertAdaptiveTextAlign("right", AdaptiveTextAlign.Right);
+ }
+
+ private static void AssertAdaptiveTextAlign(string expectedPropertyValue, AdaptiveTextAlign align)
+ {
+ AssertAdaptiveTextPropertyValue("hint-align", expectedPropertyValue, new AdaptiveText()
+ {
+ HintAlign = align
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMaxLines_MinValue()
+ {
+ AssertAdaptiveTextPropertyValue("hint-maxLines", "1", new AdaptiveText()
+ {
+ HintMaxLines = 1
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMaxLines_NormalValue()
+ {
+ AssertAdaptiveTextPropertyValue("hint-maxLines", "3", new AdaptiveText()
+ {
+ HintMaxLines = 3
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMaxLines_MaxValue()
+ {
+ AssertAdaptiveTextPropertyValue("hint-maxLines", int.MaxValue.ToString(), new AdaptiveText()
+ {
+ HintMaxLines = int.MaxValue
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMaxLines_BelowMin()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ new AdaptiveText() { HintMaxLines = 0 };
+ }, "ArgumentOutOfRangeExceptions should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMaxLines_AboveMax()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ new AdaptiveText() { HintMaxLines = -54 };
+ }, "ArgumentOutOfRangeExceptions should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMinLines_MinValue()
+ {
+ AssertAdaptiveTextPropertyValue("hint-minLines", "1", new AdaptiveText()
+ {
+ HintMinLines = 1
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMinLines_NormalValue()
+ {
+ AssertAdaptiveTextPropertyValue("hint-minLines", "3", new AdaptiveText()
+ {
+ HintMinLines = 3
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMinLines_MaxValue()
+ {
+ AssertAdaptiveTextPropertyValue("hint-minLines", int.MaxValue.ToString(), new AdaptiveText()
+ {
+ HintMinLines = int.MaxValue
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMinLines_BelowMin()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ new AdaptiveText() { HintMinLines = 0 };
+ }, "ArgumentOutOfRangeExceptions should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintMinLines_AboveMax()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ new AdaptiveText() { HintMinLines = -54 };
+ }, "ArgumentOutOfRangeExceptions should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_HintWrap_Values()
+ {
+ AssertAdaptiveTextPropertyValue("hint-wrap", "false", new AdaptiveText()
+ {
+ HintWrap = false
+ });
+
+ AssertAdaptiveTextPropertyValue("hint-wrap", "true", new AdaptiveText()
+ {
+ HintWrap = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Text_DefaultNullValues()
+ {
+ AssertAdaptiveChild(" ", new AdaptiveText()
+ {
+ HintAlign = AdaptiveTextAlign.Default,
+ HintStyle = AdaptiveTextStyle.Default,
+ HintMaxLines = null,
+ HintMinLines = null,
+ HintWrap = null,
+ Language = null,
+ Text = null
+ });
+ }
+
+ private static void AssertAdaptiveTextPropertyValue(string expectedPropertyName, string expectedPropertyValue, AdaptiveText text)
+ {
+ AssertAdaptiveChild($" ", text);
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_Defaults()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ AssertAdaptiveChild("exception should be thrown", new AdaptiveImage());
+ }, "NullReferenceException should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_Source()
+ {
+ AssertAdaptiveImagePropertyValue("src", "ms-appdata:///local/MyImage.png", new AdaptiveImage()
+ {
+ Source = "ms-appdata:///local/MyImage.png"
+ });
+
+ AssertAdaptiveImagePropertyValue("src", "ms-appx:///Assets/MyImage.png", new AdaptiveImage()
+ {
+ Source = "ms-appx:///Assets/MyImage.png"
+ });
+
+ AssertAdaptiveImagePropertyValue("src", "http://msn.com/img.png", new AdaptiveImage()
+ {
+ Source = "http://msn.com/img.png"
+ });
+
+ AssertAdaptiveImagePropertyValue("src", "Assets/MyImage.png", new AdaptiveImage()
+ {
+ Source = "Assets/MyImage.png"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_Source_Null()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ new AdaptiveImage()
+ {
+ Source = null
+ };
+ }, "ArgumentNullException should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_AddImageQuery()
+ {
+ AssertAdaptiveImagePropertyValue("addImageQuery", "false", new AdaptiveImage()
+ {
+ AddImageQuery = false
+ });
+
+ AssertAdaptiveImagePropertyValue("addImageQuery", "true", new AdaptiveImage()
+ {
+ AddImageQuery = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_AlternateText()
+ {
+ AssertAdaptiveImagePropertyValue("alt", "image of puppies", new AdaptiveImage()
+ {
+ AlternateText = "image of puppies"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_HintAlign()
+ {
+ AssertAdaptiveImageAlign("stretch", AdaptiveImageAlign.Stretch);
+ AssertAdaptiveImageAlign("left", AdaptiveImageAlign.Left);
+ AssertAdaptiveImageAlign("center", AdaptiveImageAlign.Center);
+ AssertAdaptiveImageAlign("right", AdaptiveImageAlign.Right);
+ }
+
+ private static void AssertAdaptiveImageAlign(string expectedValue, AdaptiveImageAlign align)
+ {
+ AssertAdaptiveImagePropertyValue("hint-align", expectedValue, new AdaptiveImage()
+ {
+ HintAlign = align
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_HintCrop()
+ {
+ AssertAdaptiveImageCrop("none", AdaptiveImageCrop.None);
+ AssertAdaptiveImageCrop("circle", AdaptiveImageCrop.Circle);
+ }
+
+ private static void AssertAdaptiveImageCrop(string expectedValue, AdaptiveImageCrop crop)
+ {
+ AssertAdaptiveImagePropertyValue("hint-crop", expectedValue, new AdaptiveImage()
+ {
+ HintCrop = crop
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_HintRemoveMargin()
+ {
+ AssertAdaptiveImagePropertyValue("hint-removeMargin", "false", new AdaptiveImage()
+ {
+ HintRemoveMargin = false
+ });
+
+ AssertAdaptiveImagePropertyValue("hint-removeMargin", "true", new AdaptiveImage()
+ {
+ HintRemoveMargin = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Image_DefaultNullValues()
+ {
+ AssertAdaptiveChild(" ", new AdaptiveImage()
+ {
+ Source = "img.png",
+ AddImageQuery = null,
+ AlternateText = null,
+ HintAlign = AdaptiveImageAlign.Default,
+ HintCrop = AdaptiveImageCrop.Default,
+ HintRemoveMargin = null
+ });
+ }
+
+ private static void AssertAdaptiveImagePropertyValue(string expectedPropertyName, string expectedPropertyValue, AdaptiveImage image)
+ {
+ bool addedSource = false;
+ if (image.Source == null)
+ {
+ image.Source = "img.png";
+ addedSource = true;
+ }
+
+ string xml = $" ";
+
+ AssertAdaptiveChild(xml, image);
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Group_Defaults()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ AssertAdaptiveChild("exception should be thrown since groups need at least one subgroup child", new AdaptiveGroup());
+ }, "InvalidOperationException should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Group_OneChild()
+ {
+ AssertAdaptiveChild(" ", new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Group_TwoChildren()
+ {
+ AssertAdaptiveChild(" ", new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup(),
+ new AdaptiveSubgroup()
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Group_ThreeChildren()
+ {
+ AssertAdaptiveChild(" ", new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup(),
+ new AdaptiveSubgroup(),
+ new AdaptiveSubgroup()
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_Defaults()
+ {
+ AssertAdaptiveSubgroup(" ", new AdaptiveSubgroup());
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_HintWeight_MinValue()
+ {
+ AssertAdaptiveSubgroupProperty("hint-weight", "1", new AdaptiveSubgroup()
+ {
+ HintWeight = 1
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_HintWeight_NormalValue()
+ {
+ AssertAdaptiveSubgroupProperty("hint-weight", "20", new AdaptiveSubgroup()
+ {
+ HintWeight = 20
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_HintWeight_MaxValue()
+ {
+ AssertAdaptiveSubgroupProperty("hint-weight", int.MaxValue.ToString(), new AdaptiveSubgroup()
+ {
+ HintWeight = int.MaxValue
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_HintWeight_JustBelowMin()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ AssertAdaptiveSubgroup("exception should be thrown", new AdaptiveSubgroup()
+ {
+ HintWeight = 0
+ });
+ }, "ArgumentOutOfRangeException should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_HintWeight_BelowMin()
+ {
+ Assert.ThrowsException(
+ () =>
+ {
+ AssertAdaptiveSubgroup("exception should be thrown", new AdaptiveSubgroup()
+ {
+ HintWeight = -53
+ });
+ }, "ArgumentOutOfRangeException should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_HintTextStacking()
+ {
+ AssertAdaptiveSubgroupTextStacking("top", AdaptiveSubgroupTextStacking.Top);
+ AssertAdaptiveSubgroupTextStacking("center", AdaptiveSubgroupTextStacking.Center);
+ AssertAdaptiveSubgroupTextStacking("bottom", AdaptiveSubgroupTextStacking.Bottom);
+ }
+
+ private static void AssertAdaptiveSubgroupTextStacking(string expectedValue, AdaptiveSubgroupTextStacking textStacking)
+ {
+ AssertAdaptiveSubgroupProperty("hint-textStacking", expectedValue, new AdaptiveSubgroup()
+ {
+ HintTextStacking = textStacking
+ });
+ }
+
+ [TestMethod]
+ public void Test_Adaptive_Subgroup_DefaultNullValues()
+ {
+ AssertAdaptiveSubgroup(" ", new AdaptiveSubgroup()
+ {
+ HintTextStacking = AdaptiveSubgroupTextStacking.Default,
+ HintWeight = null
+ });
+ }
+
+ private static void AssertAdaptiveSubgroupProperty(string expectedPropertyName, string expectedPropertyValue, AdaptiveSubgroup subgroup)
+ {
+ AssertAdaptiveSubgroup($" ", subgroup);
+ }
+
+ private static void AssertAdaptiveSubgroup(string expectedSubgroupXml, AdaptiveSubgroup subgroup)
+ {
+ AdaptiveGroup group = new AdaptiveGroup()
+ {
+ Children =
+ {
+ subgroup
+ }
+ };
+
+ AssertAdaptiveChild("" + expectedSubgroupXml + " ", group);
+ }
+
+ private static void AssertAdaptiveChild(string expectedAdaptiveChildXml, IAdaptiveChild child)
+ {
+ AssertAdaptiveChildInToast(expectedAdaptiveChildXml, child);
+ AssertAdaptiveChildInTile(expectedAdaptiveChildXml, child);
+
+ // Also assert them within group/subgroup if possible!
+ if (child is IAdaptiveSubgroupChild)
+ {
+ AdaptiveGroup group = new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ child as IAdaptiveSubgroupChild
+ }
+ }
+ }
+ };
+
+ string expectedGroupXml = "" + expectedAdaptiveChildXml + " ";
+
+ AssertAdaptiveChildInToast(expectedGroupXml, group);
+ AssertAdaptiveChildInTile(expectedGroupXml, group);
+ }
+ }
+
+ private static void AssertAdaptiveChildInToast(string expectedAdaptiveChildXml, IAdaptiveChild child)
+ {
+ var binding = new ToastBindingGeneric();
+
+ // If the child isn't text, we need to add a text element so notification is valid
+ if (!(child is AdaptiveText))
+ {
+ binding.Children.Add(new AdaptiveText()
+ {
+ Text = "Required text element"
+ });
+
+ expectedAdaptiveChildXml = "Required text element " + expectedAdaptiveChildXml;
+ }
+
+ binding.Children.Add((IToastBindingGenericChild)child);
+
+ var content = new ToastContent()
+ {
+ Visual = new ToastVisual()
+ {
+ BindingGeneric = binding
+ }
+ };
+
+ string expectedFinalXml = "" + expectedAdaptiveChildXml + " ";
+
+ AssertHelper.AssertToast(expectedFinalXml, content);
+ }
+
+ private static void AssertAdaptiveChildInTile(string expectedAdaptiveChildXml, IAdaptiveChild child)
+ {
+ var content = new TileContent()
+ {
+ Visual = new TileVisual()
+ {
+ TileMedium = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ (ITileBindingContentAdaptiveChild)child
+ }
+ }
+ }
+ }
+ };
+
+ string expectedFinalXml = "" + expectedAdaptiveChildXml + " ";
+
+ AssertHelper.AssertTile(expectedFinalXml, content);
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/Test_Badge_Xml.cs b/components/Notifications/tests/Test_Badge_Xml.cs
new file mode 100644
index 000000000..5ed17506f
--- /dev/null
+++ b/components/Notifications/tests/Test_Badge_Xml.cs
@@ -0,0 +1,77 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class Test_Badge_Xml
+ {
+ [TestMethod]
+ public void Test_Badge_Xml_Numeric_0()
+ {
+ AssertBadgeValue("0", new BadgeNumericContent(0));
+ }
+
+ [TestMethod]
+ public void Test_Badge_Xml_Numeric_1()
+ {
+ AssertBadgeValue("1", new BadgeNumericContent(1));
+ }
+
+ [TestMethod]
+ public void Test_Badge_Xml_Numeric_2()
+ {
+ AssertBadgeValue("2", new BadgeNumericContent(2));
+ }
+
+ [TestMethod]
+ public void Test_Badge_Xml_Numeric_546()
+ {
+ AssertBadgeValue("546", new BadgeNumericContent(546));
+ }
+
+ [TestMethod]
+ public void Test_Badge_Xml_Numeric_Max()
+ {
+ AssertBadgeValue(uint.MaxValue.ToString(), new BadgeNumericContent(uint.MaxValue));
+ }
+
+ [TestMethod]
+ public void Test_Badge_Xml_Glyph_None()
+ {
+ AssertBadgeValue("none", new BadgeGlyphContent(BadgeGlyphValue.None));
+ }
+
+ [TestMethod]
+ public void Test_Badge_Xml_Glyph_Alert()
+ {
+ AssertBadgeValue("alert", new BadgeGlyphContent(BadgeGlyphValue.Alert));
+ }
+
+ [TestMethod]
+ public void Test_Badge_Xml_Glyph_Error()
+ {
+ AssertBadgeValue("error", new BadgeGlyphContent(BadgeGlyphValue.Error));
+ }
+
+ private static void AssertBadgeValue(string expectedValue, INotificationContent notificationContent)
+ {
+ AssertPayload(" ", notificationContent);
+ }
+
+ private static void AssertPayload(string expectedXml, INotificationContent notificationContent)
+ {
+ AssertHelper.AssertXml(expectedXml, notificationContent.GetContent());
+
+ AssertHelper.AssertXml(expectedXml, notificationContent.GetXml().GetXml());
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/Test_Tile_Xml.cs b/components/Notifications/tests/Test_Tile_Xml.cs
new file mode 100644
index 000000000..f648c2b69
--- /dev/null
+++ b/components/Notifications/tests/Test_Tile_Xml.cs
@@ -0,0 +1,1679 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using System;
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class Test_Tile_Xml
+ {
+ [TestMethod]
+ public void Test_Tile_Xml_Tile_Default()
+ {
+ TileContent tile = new TileContent();
+
+ AssertPayload(" ", tile);
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Default()
+ {
+ // Assert the defaults
+ AssertVisual(" ", new TileVisual());
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_AddImageQuery_False()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ AddImageQuery = false
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_AddImageQuery_True()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ AddImageQuery = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_BaseUri_Null()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ BaseUri = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_BaseUri_Value()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ BaseUri = new Uri("http://msn.com")
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Branding_Auto()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Branding = TileBranding.Auto
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Branding_Name()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Branding = TileBranding.Name
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Branding_Logo()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Branding = TileBranding.Logo
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Branding_NameAndLogo()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Branding = TileBranding.NameAndLogo
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Branding_None()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Branding = TileBranding.None
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_ContentId_Null()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ ContentId = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_ContentId_Value()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ ContentId = "tsla"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_DisplayName_Null()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ DisplayName = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_DisplayName_Value()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ DisplayName = "My name"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Language_Null()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Language = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Language_Value()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Language = "en-US"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Arguments_Null()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Arguments = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Arguments_EmptyString()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Arguments = string.Empty
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_Arguments_Value()
+ {
+ AssertVisual(
+ " ",
+ new TileVisual()
+ {
+ Arguments = "action=viewStory&story=53"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus1_NoMatchingText()
+ {
+ AssertVisual(
+ "Awesome Cool ",
+ new TileVisual()
+ {
+ LockDetailedStatus1 = "Status 1",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveText() { Text = "Cool" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus1_MatchingText_InBinding()
+ {
+ AssertVisual(
+ "Awesome Cool Status 1 Blah ",
+ new TileVisual()
+ {
+ LockDetailedStatus1 = "Status 1",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveText() { Text = "Cool" },
+ new AdaptiveText() { Text = "Status 1" },
+ new AdaptiveText() { Text = "Blah" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus1_MatchingText_InSubgroup()
+ {
+ // The lockscreen only looks at ID's in the immediate binding children. So anything in the groups/subgroups are
+ // ignored. Thus, if text matches there, it still has to be placed as a hint.
+ AssertVisual(
+ "Awesome Status 1 Cool Blah ",
+ new TileVisual()
+ {
+ LockDetailedStatus1 = "Status 1",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveImage()
+ {
+ Source = "Fable.jpg"
+ },
+ new AdaptiveText() { Text = "Status 1" },
+ new AdaptiveText() { Text = "Cool" }
+ }
+ }
+ }
+ },
+ new AdaptiveText() { Text = "Blah" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus2_NoMatchingText()
+ {
+ AssertVisual(
+ "Awesome Cool ",
+ new TileVisual()
+ {
+ LockDetailedStatus2 = "Status 2",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveText() { Text = "Cool" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus2_MatchingText_InBinding()
+ {
+ AssertVisual(
+ "Awesome Cool Status 2 Blah ",
+ new TileVisual()
+ {
+ LockDetailedStatus2 = "Status 2",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveText() { Text = "Cool" },
+ new AdaptiveText() { Text = "Status 2" },
+ new AdaptiveText() { Text = "Blah" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus2_MatchingText_InSubgroup()
+ {
+ AssertVisual(
+ "Awesome Status 2 Cool Blah ",
+ new TileVisual()
+ {
+ LockDetailedStatus2 = "Status 2",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveImage()
+ {
+ Source = "Fable.jpg"
+ },
+ new AdaptiveText() { Text = "Status 2" },
+ new AdaptiveText() { Text = "Cool" }
+ }
+ }
+ }
+ },
+ new AdaptiveText() { Text = "Blah" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus3_NoMatchingText()
+ {
+ AssertVisual(
+ "Awesome Cool ",
+ new TileVisual()
+ {
+ LockDetailedStatus3 = "Status 3",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveText() { Text = "Cool" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus3_MatchingText_InBinding()
+ {
+ AssertVisual(
+ "Awesome Cool Status 3 Blah ",
+ new TileVisual()
+ {
+ LockDetailedStatus3 = "Status 3",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveText() { Text = "Cool" },
+ new AdaptiveText() { Text = "Status 3" },
+ new AdaptiveText() { Text = "Blah" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Visual_LockDetailedStatus3_MatchingText_InSubgroup()
+ {
+ AssertVisual(
+ "Awesome Status 3 Cool Blah ",
+ new TileVisual()
+ {
+ LockDetailedStatus3 = "Status 3",
+
+ TileWide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveText() { Text = "Awesome" },
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ Children =
+ {
+ new AdaptiveImage()
+ {
+ Source = "Fable.jpg"
+ },
+ new AdaptiveText() { Text = "Status 3" },
+ new AdaptiveText() { Text = "Cool" }
+ }
+ }
+ }
+ },
+ new AdaptiveText() { Text = "Blah" }
+ }
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Default()
+ {
+ AssertBindingMedium(" ", new TileBinding());
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_AddImageQuery_False()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ AddImageQuery = false
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_AddImageQuery_True()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ AddImageQuery = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_BaseUri_Null()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ BaseUri = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_BaseUri_Value()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ BaseUri = new Uri("http://msn.com")
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Branding_Auto()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Branding = TileBranding.Auto
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Branding_None()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Branding = TileBranding.None
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Branding_Name()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Branding = TileBranding.Name
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Branding_Logo()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Branding = TileBranding.Logo
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Branding_NameAndLogo()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Branding = TileBranding.NameAndLogo
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_ContentId_Null()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ ContentId = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_ContentId_Value()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ ContentId = "myId"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_DisplayName_Null()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ DisplayName = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_DisplayName_Value()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ DisplayName = "My name"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Language_Null()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Language = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Language_Value()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Language = "en-US"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Arguments_Null()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Arguments = null
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Arguments_EmptyString()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Arguments = string.Empty
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Binding_Arguments_Value()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Arguments = "action=viewStory&storyId=52"
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_Defaults()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_BackgroundImage_Value()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com/image.png"
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_Overlay_Default()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ HintOverlay = 20,
+ Source = "Fable.jpg"
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_Overlay_Min()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ HintOverlay = 0,
+ Source = "Fable.jpg"
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_Overlay_Max()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ HintOverlay = 100,
+ Source = "Fable.jpg"
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_Overlay_AboveDefault()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ HintOverlay = 40,
+ Source = "Fable.jpg"
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_Overlay_BelowDefault()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ HintOverlay = 10,
+ Source = "Fable.jpg"
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_BackgroundImage_Overlay_BelowMin()
+ {
+ try
+ {
+ new TileBackgroundImage()
+ {
+ HintOverlay = -1,
+ Source = "Fable.jpg"
+ };
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_Overlay_AboveMax()
+ {
+ try
+ {
+ new TileBackgroundImage()
+ {
+ HintOverlay = 101,
+ Source = "Fable.jpg"
+ };
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_PeekImage_Value()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com",
+ AlternateText = "alt",
+ AddImageQuery = true
+ }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_TextStacking_Top()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ TextStacking = TileTextStacking.Top
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_TextStacking_Center()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ TextStacking = TileTextStacking.Center
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_Root_TextStacking_Bottom()
+ {
+ AssertBindingMedium(
+ " ",
+ new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ TextStacking = TileTextStacking.Bottom
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_Defaults()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com"
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_Source()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com",
+ AddImageQuery = true,
+ AlternateText = "MSN Image"
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_Crop_None()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com",
+ HintCrop = TileBackgroundImageCrop.None
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_Crop_Circle()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com",
+ HintCrop = TileBackgroundImageCrop.Circle
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_Overlay_0()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com",
+ HintOverlay = 0
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_Overlay_20()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com",
+ HintOverlay = 20
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_Overlay_80()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "http://msn.com",
+ HintOverlay = 80
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundImage_NoImageSource()
+ {
+ try
+ {
+ TileContent c = new TileContent()
+ {
+ Visual = new TileVisual()
+ {
+ TileMedium = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ // No source, which should throw exception
+ }
+ }
+ }
+ }
+ };
+
+ c.GetContent();
+ }
+ catch (NullReferenceException)
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown");
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_Defaults()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com"
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_Source()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com",
+ AddImageQuery = true,
+ AlternateText = "MSN Image"
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_Crop_None()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com",
+ HintCrop = TilePeekImageCrop.None
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_Crop_Circle()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com",
+ HintCrop = TilePeekImageCrop.Circle
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_Overlay_0()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com",
+ HintOverlay = 0
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_Overlay_20()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com",
+ HintOverlay = 20
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_Overlay_80()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ Source = "http://msn.com",
+ HintOverlay = 80
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_PeekImage_NoImageSource()
+ {
+ try
+ {
+ TileContent c = new TileContent()
+ {
+ Visual = new TileVisual()
+ {
+ TileMedium = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ PeekImage = new TilePeekImage()
+ {
+ // No source, which should throw exception when content retrieved
+ }
+ }
+ }
+ }
+ };
+
+ c.GetContent();
+ }
+ catch (NullReferenceException)
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown");
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Defaults()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "Background.jpg"
+ },
+
+ PeekImage = new TilePeekImage()
+ {
+ Source = "Peek.jpg"
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_0and0()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "Background.jpg",
+ HintOverlay = 0
+ },
+
+ PeekImage = new TilePeekImage()
+ {
+ Source = "Peek.jpg",
+ HintOverlay = 0
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_20and20()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "Background.jpg",
+ HintOverlay = 20
+ },
+
+ PeekImage = new TilePeekImage()
+ {
+ Source = "Peek.jpg",
+ HintOverlay = 20
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_20and30()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "Background.jpg",
+ HintOverlay = 20
+ },
+
+ PeekImage = new TilePeekImage()
+ {
+ Source = "Peek.jpg",
+ HintOverlay = 30
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_30and20()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "Background.jpg",
+ HintOverlay = 30
+ },
+
+ PeekImage = new TilePeekImage()
+ {
+ Source = "Peek.jpg",
+ HintOverlay = 20
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Adaptive_BackgroundAndPeekImage_Overlay_0and20()
+ {
+ AssertBindingMediumAdaptive(
+ " ",
+ new TileBindingContentAdaptive()
+ {
+ BackgroundImage = new TileBackgroundImage()
+ {
+ Source = "Background.jpg",
+ HintOverlay = 0
+ },
+
+ PeekImage = new TilePeekImage()
+ {
+ Source = "Peek.jpg",
+ HintOverlay = 20
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Photos_Default()
+ {
+ TileBindingContentPhotos content = new TileBindingContentPhotos()
+ {
+ };
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Photos_OneImage()
+ {
+ TileBindingContentPhotos content = new TileBindingContentPhotos()
+ {
+ Images =
+ {
+ new TileBasicImage()
+ {
+ Source = "http://msn.com/1.jpg",
+ AddImageQuery = true,
+ AlternateText = "alternate"
+ }
+ }
+ };
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Photos_TwoImages()
+ {
+ TileBindingContentPhotos content = new TileBindingContentPhotos()
+ {
+ Images =
+ {
+ new TileBasicImage()
+ {
+ Source = "Assets/1.jpg"
+ },
+ new TileBasicImage()
+ {
+ Source = "Assets/2.jpg"
+ }
+ }
+ };
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Photos_MaxImages()
+ {
+ TileBindingContentPhotos content = new TileBindingContentPhotos()
+ {
+ Images =
+ {
+ new TileBasicImage() { Source = "1.jpg" },
+ new TileBasicImage() { Source = "2.jpg" },
+ new TileBasicImage() { Source = "3.jpg" },
+ new TileBasicImage() { Source = "4.jpg" },
+ new TileBasicImage() { Source = "5.jpg" },
+ new TileBasicImage() { Source = "6.jpg" },
+ new TileBasicImage() { Source = "7.jpg" },
+ new TileBasicImage() { Source = "8.jpg" },
+ new TileBasicImage() { Source = "9.jpg" },
+ new TileBasicImage() { Source = "10.jpg" },
+ new TileBasicImage() { Source = "11.jpg" },
+ new TileBasicImage() { Source = "12.jpg" }
+ }
+ };
+
+ AssertBindingMedium(
+ @"
+
+
+
+
+
+
+
+
+
+
+
+
+ ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Photos_TooManyImages()
+ {
+ try
+ {
+ new TileBindingContentPhotos()
+ {
+ Images =
+ {
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage(),
+ new TileBasicImage()
+ }
+ };
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown, adding more than 12 images isn't supported.");
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_People_Defaults()
+ {
+ TileBindingContentPeople content = new TileBindingContentPeople();
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_People_OneImage()
+ {
+ TileBindingContentPeople content = new TileBindingContentPeople()
+ {
+ Images =
+ {
+ new TileBasicImage()
+ {
+ Source = "http://msn.com/1.jpg",
+ AddImageQuery = true,
+ AlternateText = "alternate"
+ }
+ }
+ };
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_People_TwoImages()
+ {
+ TileBindingContentPeople content = new TileBindingContentPeople()
+ {
+ Images =
+ {
+ new TileBasicImage() { Source = "Assets/1.jpg" },
+ new TileBasicImage() { Source = "Assets/2.jpg" }
+ }
+ };
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_People_ManyImages()
+ {
+ string payload = "";
+
+ TileBindingContentPeople content = new TileBindingContentPeople();
+
+ // Add 30 images
+ for (int i = 1; i <= 30; i++)
+ {
+ string src = i + ".jpg";
+
+ content.Images.Add(new TileBasicImage() { Source = src });
+ payload += $" ";
+ }
+
+ payload += " ";
+
+ AssertBindingMedium(payload, new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Contact_Defaults()
+ {
+ TileBindingContentContact content = new TileBindingContentContact();
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Contact_Text()
+ {
+ TileBindingContentContact content = new TileBindingContentContact()
+ {
+ Text = new TileBasicText()
+ {
+ Text = "Hello world",
+ Lang = "en-US"
+ }
+ };
+
+ AssertBindingMedium("Hello world ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Contact_Image()
+ {
+ TileBindingContentContact content = new TileBindingContentContact()
+ {
+ Image = new TileBasicImage()
+ {
+ Source = "http://msn.com/img.jpg",
+ AddImageQuery = true,
+ AlternateText = "John Smith"
+ }
+ };
+
+ AssertBindingMedium(" ", new TileBinding()
+ {
+ Content = content
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Contact_Both_Small()
+ {
+ TileBindingContentContact content = new TileBindingContentContact()
+ {
+ Text = new TileBasicText()
+ {
+ Text = "Hello world"
+ },
+
+ Image = new TileBasicImage() { Source = "Assets/img.jpg" }
+ };
+
+ // Small doesn't support the text, so it doesn't output the text element when rendered for small
+ AssertVisual(" ", new TileVisual()
+ {
+ TileSmall = new TileBinding() { Content = content }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Contact_Both_Medium()
+ {
+ TileBindingContentContact content = new TileBindingContentContact()
+ {
+ Text = new TileBasicText()
+ {
+ Text = "Hello world"
+ },
+
+ Image = new TileBasicImage() { Source = "Assets/img.jpg" }
+ };
+
+ // Text is written before the image element
+ AssertVisual("Hello world ", new TileVisual()
+ {
+ TileMedium = new TileBinding() { Content = content }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Iconic_Small()
+ {
+ AssertVisual(" ", new TileVisual()
+ {
+ TileSmall = new TileBinding() { Content = new TileBindingContentIconic() }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Iconic_Medium()
+ {
+ AssertVisual(" ", new TileVisual()
+ {
+ TileMedium = new TileBinding() { Content = new TileBindingContentIconic() }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Tile_Xml_Special_Iconic_Image()
+ {
+ AssertVisual(" ", new TileVisual()
+ {
+ TileMedium = new TileBinding()
+ {
+ Content = new TileBindingContentIconic()
+ {
+ Icon = new TileBasicImage()
+ {
+ Source = "Assets/Iconic.png",
+ AlternateText = "iconic"
+ }
+ }
+ }
+ });
+ }
+
+ private static void AssertBindingMediumAdaptive(string expectedBindingXml, TileBindingContentAdaptive content)
+ {
+ AssertBindingMedium(expectedBindingXml, new TileBinding() { Content = content });
+ }
+
+ private static void AssertBindingMedium(string expectedBindingXml, TileBinding binding)
+ {
+ AssertVisual("" + expectedBindingXml + " ", new TileVisual()
+ {
+ TileMedium = binding
+ });
+ }
+
+ private static void AssertVisual(string expectedVisualXml, TileVisual visual)
+ {
+ AssertPayload("" + expectedVisualXml + " ", new TileContent()
+ {
+ Visual = visual
+ });
+ }
+
+ private static void AssertPayload(string expectedXml, TileContent tile)
+ {
+ AssertHelper.AssertTile(expectedXml, tile);
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/Test_Toast_Xml.cs b/components/Notifications/tests/Test_Toast_Xml.cs
new file mode 100644
index 000000000..e1144a6ec
--- /dev/null
+++ b/components/Notifications/tests/Test_Toast_Xml.cs
@@ -0,0 +1,2195 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using System;
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class Test_Toast_Xml
+ {
+ [TestMethod]
+ public void Test_Toast_XML_Toast_Defaults()
+ {
+ AssertPayload(" ", new ToastContent());
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_Launch_Value()
+ {
+ var toast = new ToastContent()
+ {
+ Launch = "tacos"
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_ActivationType_Foreground()
+ {
+ var toast = new ToastContent()
+ {
+ ActivationType = ToastActivationType.Foreground
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_ActivationType_Background()
+ {
+ var toast = new ToastContent()
+ {
+ ActivationType = ToastActivationType.Background
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_ActivationType_Protocol()
+ {
+ var toast = new ToastContent()
+ {
+ ActivationType = ToastActivationType.Protocol
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_Scenario_Default()
+ {
+ var toast = new ToastContent()
+ {
+ Scenario = ToastScenario.Default
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_Scenarios()
+ {
+ AssertToastScenario(ToastScenario.Reminder, "reminder");
+ AssertToastScenario(ToastScenario.Alarm, "alarm");
+ AssertToastScenario(ToastScenario.IncomingCall, "incomingCall");
+ }
+
+ private void AssertToastScenario(ToastScenario scenario, string scenarioText)
+ {
+ var toast = new ToastContent()
+ {
+ Scenario = scenario
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_Duration_Short()
+ {
+ var toast = new ToastContent()
+ {
+ Duration = ToastDuration.Short
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_Duration_Long()
+ {
+ var toast = new ToastContent()
+ {
+ Duration = ToastDuration.Long
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_HintToastId()
+ {
+ var toast = new ToastContent()
+ {
+ HintToastId = "AppointmentReminder"
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_HintPeople_RemoteId()
+ {
+ var toast = new ToastContent()
+ {
+ HintPeople = new ToastPeople()
+ {
+ RemoteId = "1234"
+ }
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_HintPeople_EmailAddress()
+ {
+ var toast = new ToastContent()
+ {
+ HintPeople = new ToastPeople()
+ {
+ EmailAddress = "johndoe@mydomain.com"
+ }
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_HintPeople_PhoneNumber()
+ {
+ var toast = new ToastContent()
+ {
+ HintPeople = new ToastPeople()
+ {
+ PhoneNumber = "888-888-8888"
+ }
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_HintPeople_Precedence()
+ {
+ // Email should take precedence over phone number
+ var toast = new ToastContent()
+ {
+ HintPeople = new ToastPeople()
+ {
+ EmailAddress = "johndoe@mydomain.com",
+ PhoneNumber = "888-888-8888"
+ }
+ };
+
+ AssertPayload(" ", toast);
+
+ // RemoteId should take precedence over phone number
+ toast.HintPeople = new ToastPeople()
+ {
+ RemoteId = "1234",
+ PhoneNumber = "888-888-8888"
+ };
+
+ AssertPayload(" ", toast);
+
+ // RemoteId should take precedence over all
+ toast.HintPeople = new ToastPeople()
+ {
+ RemoteId = "1234",
+ PhoneNumber = "888-888-8888",
+ EmailAddress = "johndoe@mydomain.com"
+ };
+
+ AssertPayload(" ", toast);
+ }
+
+ [TestMethod]
+ public void Test_Toast_XML_Toast_AdditionalProperties()
+ {
+ AssertPayload(" ", new ToastContent()
+ {
+ AdditionalProperties =
+ {
+ { "hint-tacos", "yummy://kind=beans,c=0" }
+ }
+ });
+
+ // Multiple
+ AssertPayload(" ", new ToastContent()
+ {
+ AdditionalProperties =
+ {
+ { "burrito", "true" },
+ { "avacado", "definitely" }
+ }
+ });
+
+ // XML encoding
+ AssertPayload(" ", new ToastContent()
+ {
+ AdditionalProperties =
+ {
+ { "request", "eggs & beans" }
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Visual_Defaults()
+ {
+ AssertPayload(" ", new ToastContent());
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Visual_AddImageQuery_False()
+ {
+ var visual = new ToastVisual()
+ {
+ AddImageQuery = false
+ };
+
+ AssertVisualPayloadProperties(@"addImageQuery='false'", visual);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Visual_AddImageQuery_True()
+ {
+ var visual = new ToastVisual()
+ {
+ AddImageQuery = true
+ };
+
+ AssertVisualPayloadProperties(@"addImageQuery=""true""", visual);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Visual_BaseUri_Value()
+ {
+ var visual = new ToastVisual()
+ {
+ BaseUri = new Uri("http://msn.com")
+ };
+
+ AssertVisualPayloadProperties(@"baseUri=""http://msn.com/""", visual);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Visual_Language_Value()
+ {
+ var visual = new ToastVisual()
+ {
+ Language = "en-US"
+ };
+
+ AssertVisualPayloadProperties(@"lang=""en-US""", visual);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Visual_AdaptiveText_Defaults()
+ {
+ AssertAdaptiveText(@" ", new AdaptiveText());
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Visual_AdaptiveText_All()
+ {
+ AssertAdaptiveText(@"Hi, I am a title ", new AdaptiveText()
+ {
+ Text = "Hi, I am a title",
+ Language = "en-US",
+ HintAlign = AdaptiveTextAlign.Right,
+ HintMaxLines = 3,
+ HintMinLines = 2,
+ HintStyle = AdaptiveTextStyle.Header,
+ HintWrap = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Xml_Attribution()
+ {
+ var visual = new ToastVisual()
+ {
+ BindingGeneric = new ToastBindingGeneric()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "My title"
+ },
+
+ new AdaptiveText()
+ {
+ Text = "My body 1"
+ }
+ },
+
+ Attribution = new ToastGenericAttributionText()
+ {
+ Text = "cnn.com"
+ }
+ }
+ };
+
+ AssertVisualPayload(@"My title My body 1 cnn.com ", visual);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Xml_Attribution_Lang()
+ {
+ var visual = new ToastVisual()
+ {
+ BindingGeneric = new ToastBindingGeneric()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "My title"
+ },
+
+ new AdaptiveText()
+ {
+ Text = "My body 1"
+ }
+ },
+
+ Attribution = new ToastGenericAttributionText()
+ {
+ Text = "cnn.com",
+ Language = "en-US"
+ }
+ }
+ };
+
+ AssertVisualPayload(@"My title My body 1 cnn.com ", visual);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_BindingGeneric_BaseUri()
+ {
+ AssertBindingGenericProperty("baseUri", "http://msn.com/images/", new ToastBindingGeneric()
+ {
+ BaseUri = new Uri("http://msn.com/images/", UriKind.Absolute)
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_BindingGeneric_AddImageQuery()
+ {
+ AssertBindingGenericProperty("addImageQuery", "false", new ToastBindingGeneric()
+ {
+ AddImageQuery = false
+ });
+
+ AssertBindingGenericProperty("addImageQuery", "true", new ToastBindingGeneric()
+ {
+ AddImageQuery = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_BindingGeneric_Language()
+ {
+ AssertBindingGenericProperty("lang", "en-US", new ToastBindingGeneric()
+ {
+ Language = "en-US"
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_BindingGeneric_DefaultNullValues()
+ {
+ AssertBindingGenericPayload(" ", new ToastBindingGeneric()
+ {
+ AddImageQuery = null,
+ AppLogoOverride = null,
+ BaseUri = null,
+ Language = null,
+ HeroImage = null,
+ Attribution = null
+ });
+ }
+
+ private static void AssertBindingGenericProperty(string expectedPropertyName, string expectedPropertyValue, ToastBindingGeneric binding)
+ {
+ AssertBindingGenericPayload($" ", binding);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_BindingShoulderTap_BaseUri()
+ {
+ AssertBindingShoulderTapProperty("baseUri", "http://msn.com/images/", new ToastBindingShoulderTap()
+ {
+ BaseUri = new Uri("http://msn.com/images/", UriKind.Absolute)
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_BindingShoulderTap_AddImageQuery()
+ {
+ AssertBindingShoulderTapProperty("addImageQuery", "false", new ToastBindingShoulderTap()
+ {
+ AddImageQuery = false
+ });
+
+ AssertBindingShoulderTapProperty("addImageQuery", "true", new ToastBindingShoulderTap()
+ {
+ AddImageQuery = true
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_BindingShoulderTap_Language()
+ {
+ AssertBindingShoulderTapProperty("lang", "en-US", new ToastBindingShoulderTap()
+ {
+ Language = "en-US"
+ });
+ }
+
+ private static void AssertBindingShoulderTapProperty(string expectedPropertyName, string expectedPropertyValue, ToastBindingShoulderTap binding)
+ {
+ AssertBindingShoulderTapPayload($" ", binding);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_ShoulderTapImage()
+ {
+ AssertShoulderTapImagePayload(" ", new ToastShoulderTapImage()
+ {
+ Source = "img.png",
+ AddImageQuery = true,
+ AlternateText = "alt text"
+ });
+
+ // Defaults shouldn't have anything assigned
+ AssertShoulderTapImagePayload(" ", new ToastShoulderTapImage()
+ {
+ Source = "img.png"
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_ShoulderTapImage_SourceRequired()
+ {
+ Assert.ThrowsException(() =>
+ {
+ AssertShoulderTapImagePayload("exception should be thrown", new ToastShoulderTapImage());
+ });
+
+ Assert.ThrowsException(() =>
+ {
+ new ToastShoulderTapImage()
+ {
+ Source = null
+ };
+ });
+ }
+
+ private static void AssertShoulderTapImagePayload(string expectedImageXml, ToastShoulderTapImage image)
+ {
+ AssertBindingShoulderTapPayload($"{expectedImageXml} ", new ToastBindingShoulderTap()
+ {
+ Image = image
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_SpriteSheet()
+ {
+ AssertSpriteSheetProperties("spritesheet-src='sprite.png' spritesheet-height='80' spritesheet-fps='25' spritesheet-startingFrame='15'", new ToastSpriteSheet()
+ {
+ Source = "sprite.png",
+ FrameHeight = 80,
+ Fps = 25,
+ StartingFrame = 15
+ });
+
+ // Defaults shouldn't have anything assigned
+ AssertSpriteSheetProperties("spritesheet-src='sprite.png'", new ToastSpriteSheet()
+ {
+ Source = "sprite.png"
+ });
+
+ // Can assign invalid values
+ AssertSpriteSheetProperties("spritesheet-src='sprite.png' spritesheet-height='0' spritesheet-fps='150' spritesheet-startingFrame='15'", new ToastSpriteSheet()
+ {
+ Source = "sprite.png",
+ FrameHeight = 0,
+ Fps = 150,
+ StartingFrame = 15
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_SpriteSheet_SourceRequired()
+ {
+ Assert.ThrowsException(() =>
+ {
+ AssertSpriteSheetProperties("exception should be thrown", new ToastSpriteSheet());
+ });
+
+ Assert.ThrowsException(() =>
+ {
+ new ToastSpriteSheet()
+ {
+ Source = null
+ };
+ });
+ }
+
+ private static void AssertSpriteSheetProperties(string expectedProperties, ToastSpriteSheet spriteSheet)
+ {
+ AssertShoulderTapImagePayload($" ", new ToastShoulderTapImage()
+ {
+ Source = "img.png",
+ SpriteSheet = spriteSheet
+ });
+ }
+
+ private static void AssertBindingShoulderTapPayload(string expectedBindingXml, ToastBindingShoulderTap binding)
+ {
+ AssertVisualPayload(" " + expectedBindingXml + " ", new ToastVisual()
+ {
+ BindingGeneric = new ToastBindingGeneric(),
+ BindingShoulderTap = binding
+ });
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_AppLogo_Crop_None()
+ {
+ var appLogo = new ToastGenericAppLogo()
+ {
+ HintCrop = ToastGenericAppLogoCrop.None,
+ Source = "img.png"
+ };
+
+ AssertAppLogoPayload(@" ", appLogo);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_AppLogo_Crop_Circle()
+ {
+ var appLogo = new ToastGenericAppLogo()
+ {
+ HintCrop = ToastGenericAppLogoCrop.Circle,
+ Source = "img.png"
+ };
+
+ AssertAppLogoPayload(@" ", appLogo);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_AppLogo_Source_Defaults()
+ {
+ var appLogo = new ToastGenericAppLogo()
+ {
+ Source = "http://xbox.com/Avatar.jpg"
+ };
+
+ AssertAppLogoPayload(@" ", appLogo);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_AppLogo_Source_Alt()
+ {
+ var appLogo = new ToastGenericAppLogo()
+ {
+ Source = "http://xbox.com/Avatar.jpg",
+ AlternateText = "alternate"
+ };
+
+ AssertAppLogoPayload(@" ", appLogo);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_AppLogo_Source_AddImageQuery_False()
+ {
+ var appLogo = new ToastGenericAppLogo()
+ {
+ Source = "http://xbox.com/Avatar.jpg",
+ AddImageQuery = false
+ };
+
+ AssertAppLogoPayload(@" ", appLogo);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_AppLogo_Source_AddImageQuery_True()
+ {
+ var appLogo = new ToastGenericAppLogo()
+ {
+ Source = "http://xbox.com/Avatar.jpg",
+ AddImageQuery = true
+ };
+
+ AssertAppLogoPayload(@" ", appLogo);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Xml_HeroImage_Default()
+ {
+ var hero = new ToastGenericHeroImage();
+
+ try
+ {
+ AssertHeroImagePayload(" ", hero);
+ }
+ catch (NullReferenceException)
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown since Source wasn't provided.");
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Xml_HeroImage_WithSource()
+ {
+ var hero = new ToastGenericHeroImage()
+ {
+ Source = "http://food.com/peanuts.jpg"
+ };
+
+ AssertHeroImagePayload(" ", hero);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Xml_HeroImage_Alt()
+ {
+ var hero = new ToastGenericHeroImage()
+ {
+ Source = "http://food.com/peanuts.jpg",
+ AlternateText = "peanuts"
+ };
+
+ AssertHeroImagePayload(" ", hero);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Xml_HeroImage_AddImageQuery()
+ {
+ var hero = new ToastGenericHeroImage()
+ {
+ Source = "http://food.com/peanuts.jpg",
+ AddImageQuery = true
+ };
+
+ AssertHeroImagePayload(" ", hero);
+ }
+
+ [TestMethod]
+ public void Test_ToastV2_Xml_HeroImage_AllProperties()
+ {
+ var hero = new ToastGenericHeroImage()
+ {
+ Source = "http://food.com/peanuts.jpg",
+ AddImageQuery = true,
+ AlternateText = "peanuts"
+ };
+
+ AssertHeroImagePayload(" ", hero);
+ }
+
+ private static ToastContent GenerateFromVisual(ToastVisual visual)
+ {
+ return new ToastContent()
+ {
+ Visual = visual
+ };
+ }
+
+ ///
+ /// Used for testing properties of visual without needing to specify the Generic binding
+ ///
+ private static void AssertVisualPayloadProperties(string expectedVisualProperties, ToastVisual visual)
+ {
+ visual.BindingGeneric = new ToastBindingGeneric();
+
+ AssertVisualPayload(" ", visual);
+ }
+
+ private static void AssertBindingGenericPayload(string expectedBindingXml, ToastBindingGeneric binding)
+ {
+ AssertVisualPayload("" + expectedBindingXml + " ", new ToastVisual()
+ {
+ BindingGeneric = binding
+ });
+ }
+
+ private static void AssertAdaptiveText(string expectedAdaptiveTextXml, AdaptiveText text)
+ {
+ AssertBindingGenericPayload("" + expectedAdaptiveTextXml + " ", new ToastBindingGeneric()
+ {
+ Children =
+ {
+ text
+ }
+ });
+ }
+
+ private static void AssertAppLogoPayload(string expectedAppLogoXml, ToastGenericAppLogo appLogo)
+ {
+ AssertVisualPayload(@"" + expectedAppLogoXml + " ", new ToastVisual()
+ {
+ BindingGeneric = new ToastBindingGeneric()
+ {
+ AppLogoOverride = appLogo
+ }
+ });
+ }
+
+ private static void AssertHeroImagePayload(string expectedHeroXml, ToastGenericHeroImage heroImage)
+ {
+ AssertVisualPayload(@"" + expectedHeroXml + " ", new ToastVisual()
+ {
+ BindingGeneric = new ToastBindingGeneric()
+ {
+ HeroImage = heroImage
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Audio_Defaults()
+ {
+ var audio = new ToastAudio();
+
+ AssertAudioPayload(" ", audio);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Audio_Loop_False()
+ {
+ var audio = new ToastAudio()
+ {
+ Loop = false
+ };
+
+ AssertAudioPayload(" ", audio);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Audio_Loop_True()
+ {
+ var audio = new ToastAudio()
+ {
+ Loop = true
+ };
+
+ AssertAudioPayload(" ", audio);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Audio_Silent_False()
+ {
+ var audio = new ToastAudio()
+ {
+ Silent = false
+ };
+
+ AssertAudioPayload(" ", audio);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Audio_Silent_True()
+ {
+ var audio = new ToastAudio()
+ {
+ Silent = true
+ };
+
+ AssertAudioPayload(" ", audio);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Audio_Src_Value()
+ {
+ var audio = new ToastAudio()
+ {
+ Src = new Uri("ms-appx:///Assets/audio.mp3")
+ };
+
+ AssertAudioPayload(" ", audio);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_SnoozeAndDismiss()
+ {
+ AssertActionsPayload(" ", new ToastActionsSnoozeAndDismiss());
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_Custom_Defaults()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom());
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_TextBoxAndButton()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ Buttons =
+ {
+ new ToastButton("Click me!", "clickArgs")
+ },
+
+ Inputs =
+ {
+ new ToastTextBox("tb1")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_TwoTextBoxes()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ Inputs =
+ {
+ new ToastTextBox("tb1"),
+ new ToastTextBox("tb2")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_FiveTextBoxes()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ Inputs =
+ {
+ new ToastTextBox("tb1"),
+ new ToastTextBox("tb2"),
+ new ToastTextBox("tb3"),
+ new ToastTextBox("tb4"),
+ new ToastTextBox("tb5")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_SixTextBoxes()
+ {
+ try
+ {
+ new ToastActionsCustom()
+ {
+ Inputs =
+ {
+ new ToastTextBox("tb1"),
+ new ToastTextBox("tb2"),
+ new ToastTextBox("tb3"),
+ new ToastTextBox("tb4"),
+ new ToastTextBox("tb5"),
+ new ToastTextBox("tb6")
+ }
+ };
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_SelectionAndButton()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ Inputs =
+ {
+ new ToastSelectionBox("s1")
+ {
+ Items =
+ {
+ new ToastSelectionBoxItem("1", "First"),
+ new ToastSelectionBoxItem("2", "Second")
+ }
+ }
+ },
+
+ Buttons =
+ {
+ new ToastButton("Click me!", "clickArgs")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_TwoButtons()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ Buttons =
+ {
+ new ToastButton("Button 1", "1"),
+ new ToastButton("Button 2", "2")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_FiveButtons()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ Buttons =
+ {
+ new ToastButton("Button 1", "1"),
+ new ToastButton("Button 2", "2"),
+ new ToastButton("Button 3", "3"),
+ new ToastButton("Button 4", "4"),
+ new ToastButton("Button 5", "5")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_SixButtons()
+ {
+ try
+ {
+ new ToastActionsCustom()
+ {
+ Buttons =
+ {
+ new ToastButton("Button 1", "1"),
+ new ToastButton("Button 2", "2"),
+ new ToastButton("Button 3", "3"),
+ new ToastButton("Button 4", "4"),
+ new ToastButton("Button 5", "5"),
+ new ToastButton("Button 6", "6")
+ }
+ };
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_SixTotal()
+ {
+ try
+ {
+ AssertActionsPayload("doesn't matter", new ToastActionsCustom()
+ {
+ Buttons =
+ {
+ new ToastButton("Button 1", "1"),
+ new ToastButton("Button 2", "2"),
+ new ToastButton("Button 3", "3"),
+ new ToastButton("Button 4", "4"),
+ new ToastButton("Button 5", "5")
+ },
+
+ ContextMenuItems =
+ {
+ new ToastContextMenuItem("Menu item 1", "1")
+ }
+ });
+ }
+ catch (InvalidOperationException)
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown, only 5 actions are allowed.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_SnoozeAndDismissUsingBuilders()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ Buttons =
+ {
+ // Allowing system to auto-generate text content
+ new ToastButton()
+ .SetSnoozeActivation(),
+
+ // Allowing system to auto-generate text content
+ new ToastButton()
+ .SetDismissActivation(),
+
+ // Specifying specific content
+ new ToastButton()
+ .SetContent("Hide")
+ .SetDismissActivation(),
+
+ new ToastButton()
+ .SetContent("Later")
+ .SetSnoozeActivation(),
+
+ new ToastButton()
+ .SetContent("Remind me")
+ .SetSnoozeActivation("snoozePicker")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_TwoContextMenuItems()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ ContextMenuItems =
+ {
+ new ToastContextMenuItem("Menu item 1", "1"),
+ new ToastContextMenuItem("Menu item 2", "2")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_FiveContextMenuItems()
+ {
+ AssertActionsPayload(" ", new ToastActionsCustom()
+ {
+ ContextMenuItems =
+ {
+ new ToastContextMenuItem("Menu item 1", "1"),
+ new ToastContextMenuItem("Menu item 2", "2"),
+ new ToastContextMenuItem("Menu item 3", "3"),
+ new ToastContextMenuItem("Menu item 4", "4"),
+ new ToastContextMenuItem("Menu item 5", "5")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_SixContextMenuItems()
+ {
+ try
+ {
+ AssertActionsPayload("doesn't matter", new ToastActionsCustom()
+ {
+ ContextMenuItems =
+ {
+ new ToastContextMenuItem("Menu item 1", "1"),
+ new ToastContextMenuItem("Menu item 2", "2"),
+ new ToastContextMenuItem("Menu item 3", "3"),
+ new ToastContextMenuItem("Menu item 4", "4"),
+ new ToastContextMenuItem("Menu item 5", "5"),
+ new ToastContextMenuItem("Menu item 6", "6")
+ }
+ });
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ActionsSnoozeDismiss_TwoContextMenuItems()
+ {
+ AssertActionsPayload(" ", new ToastActionsSnoozeAndDismiss()
+ {
+ ContextMenuItems =
+ {
+ new ToastContextMenuItem("Menu item 1", "1"),
+ new ToastContextMenuItem("Menu item 2", "2")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_ActionsSnoozeDismiss_FiveContextMenuItems()
+ {
+ AssertActionsPayload(" ", new ToastActionsSnoozeAndDismiss()
+ {
+ ContextMenuItems =
+ {
+ new ToastContextMenuItem("Menu item 1", "1"),
+ new ToastContextMenuItem("Menu item 2", "2"),
+ new ToastContextMenuItem("Menu item 3", "3"),
+ new ToastContextMenuItem("Menu item 4", "4"),
+ new ToastContextMenuItem("Menu item 5", "5")
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Actions_ActionsSnoozeDismiss_SixContextMenuItems()
+ {
+ try
+ {
+ AssertActionsPayload("doesn't matter", new ToastActionsSnoozeAndDismiss()
+ {
+ ContextMenuItems =
+ {
+ new ToastContextMenuItem("Menu item 1", "1"),
+ new ToastContextMenuItem("Menu item 2", "2"),
+ new ToastContextMenuItem("Menu item 3", "3"),
+ new ToastContextMenuItem("Menu item 4", "4"),
+ new ToastContextMenuItem("Menu item 5", "5"),
+ new ToastContextMenuItem("Menu item 6", "6")
+ }
+ });
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_Defaults()
+ {
+ ToastButton button = new ToastButton("my content", "myArgs");
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_NullContent()
+ {
+ try
+ {
+ new ToastButton(null, "args");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_NullArguments()
+ {
+ try
+ {
+ new ToastButton("content", null);
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_ActivationType_Foreground()
+ {
+ ToastButton button = new ToastButton("my content", "myArgs")
+ {
+ ActivationType = ToastActivationType.Foreground
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_ActivationType_Background()
+ {
+ ToastButton button = new ToastButton("my content", "myArgs")
+ {
+ ActivationType = ToastActivationType.Background
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_ActivationType_Protocol()
+ {
+ ToastButton button = new ToastButton("my content", "myArgs")
+ {
+ ActivationType = ToastActivationType.Protocol
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_ImageUri_Value()
+ {
+ ToastButton button = new ToastButton("my content", "myArgs")
+ {
+ ImageUri = "Assets/button.png"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_TextBoxId_Value()
+ {
+ ToastButton button = new ToastButton("my content", "myArgs")
+ {
+ TextBoxId = "myTextBox"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_Button_HintActionId_Value()
+ {
+ ToastButton button = new ToastButton("my content", "myArgs")
+ {
+ HintActionId = "MyAction1"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonSnooze_Defaults()
+ {
+ ToastButtonSnooze button = new ToastButtonSnooze();
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonSnooze_CustomContent()
+ {
+ ToastButtonSnooze button = new ToastButtonSnooze("my snooze");
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonSnooze_Image()
+ {
+ ToastButtonSnooze button = new ToastButtonSnooze()
+ {
+ ImageUri = "Assets/Snooze.png"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonSnooze_SelectionId()
+ {
+ ToastButtonSnooze button = new ToastButtonSnooze()
+ {
+ SelectionBoxId = "snoozeId"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonSnooze_HintActionId()
+ {
+ ToastButtonSnooze button = new ToastButtonSnooze()
+ {
+ HintActionId = "MySnoozeButton1"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonDismiss_Defaults()
+ {
+ ToastButtonDismiss button = new ToastButtonDismiss();
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonDismiss_CustomContent()
+ {
+ ToastButtonDismiss button = new ToastButtonDismiss("my dismiss");
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonDismiss_Image()
+ {
+ ToastButtonDismiss button = new ToastButtonDismiss()
+ {
+ ImageUri = "Assets/Dismiss.png"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ButtonDismiss_HintActionId()
+ {
+ ToastButtonDismiss button = new ToastButtonDismiss()
+ {
+ HintActionId = "MyDismissButton1"
+ };
+
+ AssertButtonPayload(" ", button);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ContextMenuItem_Defaults()
+ {
+ ToastContextMenuItem item = new ToastContextMenuItem("content", "args");
+
+ AssertContextMenuItemPayload(" ", item);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ContextMenuItem_NullContent()
+ {
+ try
+ {
+ new ToastContextMenuItem(null, "args");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ContextMenuItem_NullArguments()
+ {
+ try
+ {
+ new ToastContextMenuItem("content", null);
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ContextMenuItem_ActivationType_Foreground()
+ {
+ ToastContextMenuItem item = new ToastContextMenuItem("content", "args")
+ {
+ ActivationType = ToastActivationType.Foreground
+ };
+
+ AssertContextMenuItemPayload(" ", item);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ContextMenuItem_ActivationType_Background()
+ {
+ ToastContextMenuItem item = new ToastContextMenuItem("content", "args")
+ {
+ ActivationType = ToastActivationType.Background
+ };
+
+ AssertContextMenuItemPayload(" ", item);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ContextMenuItem_ActivationType_Protocol()
+ {
+ ToastContextMenuItem item = new ToastContextMenuItem("content", "args")
+ {
+ ActivationType = ToastActivationType.Protocol
+ };
+
+ AssertContextMenuItemPayload(" ", item);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_ContextMenuItem_HintActionId()
+ {
+ ToastContextMenuItem item = new ToastContextMenuItem("content", "args")
+ {
+ HintActionId = "MyContextMenu1"
+ };
+
+ AssertContextMenuItemPayload(" ", item);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_TextBox_Defaults()
+ {
+ var textBox = new ToastTextBox("myId");
+
+ AssertInputPayload(" ", textBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_TextBox_DefaultTextInput_Value()
+ {
+ var textBox = new ToastTextBox("myId")
+ {
+ DefaultInput = "Default text input"
+ };
+
+ AssertInputPayload(" ", textBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_TextBox_PlaceholderContent_Value()
+ {
+ var textBox = new ToastTextBox("myId")
+ {
+ PlaceholderContent = "My placeholder content"
+ };
+
+ AssertInputPayload(" ", textBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_TextBox_Title_Value()
+ {
+ var textBox = new ToastTextBox("myId")
+ {
+ Title = "My title"
+ };
+
+ AssertInputPayload(" ", textBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_TextBox_NullId()
+ {
+ try
+ {
+ new ToastTextBox(null);
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_TextBox_EmptyId()
+ {
+ var textBox = new ToastTextBox(string.Empty);
+
+ AssertInputPayload(" ", textBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBox_Defaults()
+ {
+ var selectionBox = new ToastSelectionBox("myId");
+
+ AssertInputPayload(" ", selectionBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBox_EmptyId()
+ {
+ var selectionBox = new ToastSelectionBox(string.Empty);
+
+ AssertInputPayload(" ", selectionBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBox_NullId()
+ {
+ try
+ {
+ new ToastSelectionBox(null);
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBox_DefaultSelectionBoxItemId_Value()
+ {
+ var selectionBox = new ToastSelectionBox("myId")
+ {
+ DefaultSelectionBoxItemId = "2"
+ };
+
+ AssertInputPayload(" ", selectionBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBox_Title_Value()
+ {
+ var selectionBox = new ToastSelectionBox("myId")
+ {
+ Title = "My title"
+ };
+
+ AssertInputPayload(" ", selectionBox);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBoxItem()
+ {
+ var selectionBoxItem = new ToastSelectionBoxItem("myId", "My content");
+
+ AssertSelectionPayload(" ", selectionBoxItem);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBoxItem_NullId()
+ {
+ try
+ {
+ new ToastSelectionBoxItem(null, "My content");
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBoxItem_NullContent()
+ {
+ try
+ {
+ new ToastSelectionBoxItem("myId", null);
+ }
+ catch
+ {
+ return;
+ }
+
+ Assert.Fail("Exception should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBoxItem_EmptyId()
+ {
+ var selectionBoxItem = new ToastSelectionBoxItem(string.Empty, "My content");
+
+ AssertSelectionPayload(" ", selectionBoxItem);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Xml_SelectionBoxItem_EmptyContent()
+ {
+ var selectionBoxItem = new ToastSelectionBoxItem("myId", string.Empty);
+
+ AssertSelectionPayload(" ", selectionBoxItem);
+ }
+
+ [TestMethod]
+ public void Test_Toast_Header_AllValues()
+ {
+ AssertHeaderPayload("", new ToastHeader("myId", "My header", "myArgs")
+ {
+ ActivationType = ToastActivationType.Protocol
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Header_NullId()
+ {
+ try
+ {
+ new ToastHeader(null, "Title", "Args");
+ }
+ catch (ArgumentNullException)
+ {
+ try
+ {
+ new ToastHeader("Id", "Title", "Args")
+ {
+ Id = null
+ };
+ }
+ catch (ArgumentNullException)
+ {
+ return;
+ }
+ }
+
+ Assert.Fail("ArgumentNullException for Id should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Header_NullTitle()
+ {
+ try
+ {
+ new ToastHeader("id", null, "Args");
+ }
+ catch (ArgumentNullException)
+ {
+ try
+ {
+ new ToastHeader("Id", "Title", "Args")
+ {
+ Title = null
+ };
+ }
+ catch (ArgumentNullException)
+ {
+ return;
+ }
+ }
+
+ Assert.Fail("ArgumentNullException for Title should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Header_NullArguments()
+ {
+ try
+ {
+ new ToastHeader("id", "Title", null);
+ }
+ catch (ArgumentNullException)
+ {
+ try
+ {
+ new ToastHeader("id", "Title", "args")
+ {
+ Arguments = null
+ };
+ }
+ catch (ArgumentNullException)
+ {
+ return;
+ }
+ }
+
+ Assert.Fail("ArgumentNullException for Arguments should have been thrown.");
+ }
+
+ [TestMethod]
+ public void Test_Toast_Header_EmptyStrings()
+ {
+ AssertHeaderPayload("", new ToastHeader(string.Empty, string.Empty, string.Empty));
+ }
+
+ [TestMethod]
+ public void Test_Toast_Header_ActivationTypes()
+ {
+ AssertHeaderActivationType("foreground", ToastActivationType.Foreground);
+
+ try
+ {
+ AssertHeaderActivationType("background", ToastActivationType.Background);
+ throw new Exception("ArgumentException should have been thrown, since activation type of background isn't allowed.");
+ }
+ catch (ArgumentException)
+ {
+ }
+
+ AssertHeaderActivationType("protocol", ToastActivationType.Protocol);
+ }
+
+ [TestMethod]
+ public void Test_Toast_DisplayTimestamp()
+ {
+ AssertPayload(" ", new ToastContent()
+ {
+ DisplayTimestamp = new DateTime(2016, 10, 19, 9, 0, 0, DateTimeKind.Utc)
+ });
+
+ AssertPayload(" ", new ToastContent()
+ {
+ DisplayTimestamp = new DateTimeOffset(2016, 10, 19, 9, 0, 0, TimeSpan.FromHours(-8))
+ });
+
+ // If devs use DateTime.Now, or directly use ticks (like this code), they can actually end up with a seconds decimal
+ // value that is more than 3 decimal places. The platform notification parser will fail if there are
+ // more than three decimal places. Hence this test normally would produce "2017-04-04T10:28:34.7047925Z"
+ // but we've added code to ensure it strips to only at most 3 decimal places.
+ AssertPayload(" ", new ToastContent()
+ {
+ DisplayTimestamp = new DateTimeOffset(636268985147047925, TimeSpan.FromHours(0))
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_Button_ActivationOptions()
+ {
+ AssertButtonPayload(" ", new ToastButton("My content", "myArgs")
+ {
+ ActivationType = ToastActivationType.Protocol,
+ ActivationOptions = new ToastActivationOptions()
+ {
+ AfterActivationBehavior = ToastAfterActivationBehavior.PendingUpdate,
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ }
+ });
+
+ // Empty class should do nothing
+ AssertButtonPayload(" ", new ToastButton("My content", "myArgs")
+ {
+ ActivationType = ToastActivationType.Background,
+ ActivationOptions = new ToastActivationOptions()
+ });
+
+ // Default should be ignored
+ AssertButtonPayload(" ", new ToastButton("My content", "myArgs")
+ {
+ ActivationType = ToastActivationType.Background,
+ ActivationOptions = new ToastActivationOptions()
+ {
+ AfterActivationBehavior = ToastAfterActivationBehavior.Default
+ }
+ });
+
+ // Specifying protocol PFN without using protocol activation should throw exception
+ try
+ {
+ AssertButtonPayload("Exception should be thrown", new ToastButton("My content", "myArgs")
+ {
+ ActivationOptions = new ToastActivationOptions()
+ {
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ }
+ });
+ Assert.Fail("InvalidOperationException should have been thrown.");
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ [TestMethod]
+ public void Test_Toast_ContextMenuItem_ActivationOptions()
+ {
+ ToastContextMenuItem item = new ToastContextMenuItem("My content", "myArgs")
+ {
+ ActivationType = ToastActivationType.Protocol,
+ ActivationOptions = new ToastActivationOptions()
+ {
+ AfterActivationBehavior = ToastAfterActivationBehavior.PendingUpdate,
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ }
+ };
+
+ AssertContextMenuItemPayload(" ", item);
+
+ // Empty class should do nothing
+ item.ActivationOptions = new ToastActivationOptions();
+
+ AssertContextMenuItemPayload(" ", item);
+
+ // Default should be ignored
+ item.ActivationOptions.AfterActivationBehavior = ToastAfterActivationBehavior.Default;
+
+ AssertContextMenuItemPayload(" ", item);
+
+ // Specifying protocol PFN without using protocol activation should throw exception
+ try
+ {
+ AssertContextMenuItemPayload("Exception should be thrown", new ToastContextMenuItem("My content", "myArgs")
+ {
+ ActivationOptions = new ToastActivationOptions()
+ {
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ }
+ });
+ Assert.Fail("InvalidOperationException should have been thrown.");
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ [TestMethod]
+ public void Test_Toast_ActivationOptions()
+ {
+ AssertPayload(" ", new ToastContent()
+ {
+ Launch = "settings:about",
+ ActivationType = ToastActivationType.Protocol,
+ ActivationOptions = new ToastActivationOptions()
+ {
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ }
+ });
+
+ // Empty class should do nothing
+ AssertPayload(" ", new ToastContent()
+ {
+ Launch = "myArgs",
+ ActivationType = ToastActivationType.Background,
+ ActivationOptions = new ToastActivationOptions()
+ });
+
+ // Default should be ignored
+ AssertPayload(" ", new ToastContent()
+ {
+ Launch = "myArgs",
+ ActivationType = ToastActivationType.Background,
+ ActivationOptions = new ToastActivationOptions()
+ {
+ AfterActivationBehavior = ToastAfterActivationBehavior.Default
+ }
+ });
+
+ // Setting anything other than default should throw exception
+ try
+ {
+ AssertPayload("XML shouldn't matter", new ToastContent()
+ {
+ Launch = "myArgs",
+ ActivationOptions = new ToastActivationOptions()
+ {
+ AfterActivationBehavior = ToastAfterActivationBehavior.PendingUpdate
+ }
+ });
+ Assert.Fail("InvalidOperationException should have been thrown.");
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ // Specifying protocol PFN without using protocol activation should throw exception
+ try
+ {
+ AssertPayload("Exception should be thrown", new ToastContent()
+ {
+ ActivationOptions = new ToastActivationOptions()
+ {
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ }
+ });
+ Assert.Fail("InvalidOperationException should have been thrown.");
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ [TestMethod]
+ public void Test_Toast_Header_ActivationOptions()
+ {
+ var header = new ToastHeader("myId", "My title", "settings:about")
+ {
+ ActivationType = ToastActivationType.Protocol,
+ ActivationOptions = new ToastActivationOptions()
+ {
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ }
+ };
+
+ AssertHeaderPayload("", header);
+
+ // Empty class should do nothing
+ header.ActivationOptions = new ToastActivationOptions();
+ AssertHeaderPayload("", header);
+
+ // Default should be ignored
+ header.ActivationOptions.AfterActivationBehavior = ToastAfterActivationBehavior.Default;
+ AssertHeaderPayload("", header);
+
+ // Using anything other than default should throw exception
+ try
+ {
+ header.ActivationOptions.AfterActivationBehavior = ToastAfterActivationBehavior.PendingUpdate;
+ AssertHeaderPayload("Exception should be thrown", header);
+ Assert.Fail("InvalidOperationException should have been thrown.");
+ }
+ catch (InvalidOperationException)
+ {
+ }
+
+ // Specifying protocol PFN without using protocol activation should throw exception
+ try
+ {
+ header.ActivationType = ToastActivationType.Foreground;
+ header.ActivationOptions = new ToastActivationOptions()
+ {
+ ProtocolActivationTargetApplicationPfn = "Microsoft.Settings"
+ };
+ AssertHeaderPayload("Exception should be thrown", header);
+ Assert.Fail("InvalidOperationException should have been thrown.");
+ }
+ catch (InvalidOperationException)
+ {
+ }
+ }
+
+ [TestMethod]
+ public void Test_Toast_ProgressBar_Value()
+ {
+ AssertProgressBar(" ", new AdaptiveProgressBar() { Status = "Downloading..." });
+
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Value = 0.3,
+ Status = "Downloading..."
+ });
+
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Value = AdaptiveProgressBarValue.FromValue(0.3),
+ Status = "Downloading..."
+ });
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Value = AdaptiveProgressBarValue.Indeterminate,
+ Status = "Downloading..."
+ });
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Value = new BindableProgressBarValue("progressValue"),
+ Status = "Downloading..."
+ });
+
+ try
+ {
+ new AdaptiveProgressBar()
+ {
+ Value = AdaptiveProgressBarValue.FromValue(-4),
+ Status = "Downloading..."
+ };
+ Assert.Fail("Exception should have been thrown, only values 0-1 allowed");
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+
+ try
+ {
+ new AdaptiveProgressBar()
+ {
+ Value = AdaptiveProgressBarValue.FromValue(1.3),
+ Status = "Downloading..."
+ };
+ Assert.Fail("Exception should have been thrown, only values 0-1 allowed");
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ }
+ }
+
+ [TestMethod]
+ public void Test_Toast_ProgressBar_NonEscapingString()
+ {
+ // There is NOT an escape string. Guidance to developers is if you're using data binding,
+ // use data binding for ALL your user-generated strings.
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Title = "{I like tacos}",
+ Status = "Downloading..."
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_ProgressBar()
+ {
+ try
+ {
+ AssertProgressBar("Exception should be thrown", new AdaptiveProgressBar());
+ Assert.Fail("Exception should have been thrown, Status property is required");
+ }
+ catch (NullReferenceException)
+ {
+ }
+
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Value = AdaptiveProgressBarValue.FromValue(0.3),
+ Title = "Katy Perry",
+ ValueStringOverride = "3/10 songs",
+ Status = "Adding music..."
+ });
+
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Value = new BindableProgressBarValue("progressValue"),
+ Title = new BindableString("progressTitle"),
+ ValueStringOverride = new BindableString("progressValueOverride"),
+ Status = new BindableString("progressStatus")
+ });
+
+ AssertProgressBar(" ", new AdaptiveProgressBar()
+ {
+ Value = null,
+ Title = null,
+ ValueStringOverride = null,
+ Status = "Downloading..."
+ });
+ }
+
+ private static void AssertProgressBar(string expectedProgressBarXml, AdaptiveProgressBar progressBar)
+ {
+ AssertBindingGenericPayload("" + expectedProgressBarXml + " ", new ToastBindingGeneric()
+ {
+ Children =
+ {
+ progressBar
+ }
+ });
+ }
+
+ [TestMethod]
+ public void Test_Toast_FullPayload_ShoulderTap()
+ {
+ var content = new ToastContent()
+ {
+ HintPeople = new ToastPeople()
+ {
+ EmailAddress = "johndoe@mydomain.com"
+ },
+
+ Visual = new ToastVisual()
+ {
+ BindingGeneric = new ToastBindingGeneric()
+ {
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "Toast fallback"
+ },
+
+ new AdaptiveText()
+ {
+ Text = "Add your fallback toast content here"
+ }
+ }
+ },
+
+ BindingShoulderTap = new ToastBindingShoulderTap()
+ {
+ Image = new ToastShoulderTapImage()
+ {
+ Source = "img.png",
+ SpriteSheet = new ToastSpriteSheet()
+ {
+ Source = "sprite.png",
+ FrameHeight = 80,
+ Fps = 25,
+ StartingFrame = 15
+ }
+ }
+ }
+ }
+ };
+
+ AssertPayload(
+ @"
+
+
+ Toast fallback
+ Add your fallback toast content here
+
+
+
+
+
+ ", content);
+ }
+
+ private static void AssertSelectionPayload(string expectedSelectionXml, ToastSelectionBoxItem selectionItem)
+ {
+ AssertInputPayload(" " + expectedSelectionXml + "", new ToastSelectionBox("myId")
+ {
+ Items = { selectionItem }
+ });
+ }
+
+ private static void AssertInputPayload(string expectedInputXml, IToastInput textBox)
+ {
+ AssertActionsPayload("" + expectedInputXml + " ", new ToastActionsCustom()
+ {
+ Inputs = { textBox }
+ });
+ }
+
+ private static void AssertButtonPayload(string expectedButtonXml, IToastButton button)
+ {
+ AssertActionsPayload("" + expectedButtonXml + " ", new ToastActionsCustom()
+ {
+ Buttons = { button }
+ });
+ }
+
+ private static void AssertContextMenuItemPayload(string expectedContextMenuItemXml, ToastContextMenuItem item)
+ {
+ AssertActionsPayload("" + expectedContextMenuItemXml + " ", new ToastActionsCustom()
+ {
+ ContextMenuItems = { item }
+ });
+ }
+
+ private static void AssertActionsPayload(string expectedActionsXml, IToastActions actions)
+ {
+ AssertPayload("" + expectedActionsXml + " ", new ToastContent()
+ {
+ Actions = actions
+ });
+ }
+
+ private static void AssertAudioPayload(string expectedAudioXml, ToastAudio audio)
+ {
+ AssertPayload("" + expectedAudioXml + " ", new ToastContent()
+ {
+ Audio = audio
+ });
+ }
+
+ private static void AssertVisualPayload(string expectedVisualXml, ToastVisual visual)
+ {
+ AssertPayload("" + expectedVisualXml + " ", new ToastContent()
+ {
+ Visual = visual
+ });
+ }
+
+ private static void AssertHeaderActivationType(string expectedPropertyValue, ToastActivationType activationType)
+ {
+ ToastHeader header = new ToastHeader("myId", "My title", "myArgs")
+ {
+ ActivationType = activationType
+ };
+
+ if (activationType == ToastActivationType.Foreground)
+ {
+ AssertHeaderPayload("", header);
+ }
+ else
+ {
+ AssertHeaderPayload($"", header);
+ }
+ }
+
+ private static void AssertHeaderPayload(string expectedHeaderXml, ToastHeader header)
+ {
+ AssertPayload("" + expectedHeaderXml + " ", new ToastContent()
+ {
+ Header = header
+ });
+ }
+
+ private static void AssertPayload(string expectedXml, ToastContent toast)
+ {
+ AssertHelper.AssertToast(expectedXml, toast);
+ }
+ }
+}
+#endif
diff --git a/components/Notifications/tests/TextXboxModern.cs b/components/Notifications/tests/TextXboxModern.cs
new file mode 100644
index 000000000..1ffd054aa
--- /dev/null
+++ b/components/Notifications/tests/TextXboxModern.cs
@@ -0,0 +1,166 @@
+// 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.
+
+#if WINDOWS_UWP
+
+using CommunityToolkit.Notifications;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+
+#nullable disable
+namespace NotificationsExperiment.Tests
+{
+ [TestClass]
+ public class TextXboxModern
+ {
+ [TestCategory("EndToEnd/XboxModern")]
+ [TestMethod]
+ public void TestXboxModernTile()
+ {
+ TileBinding medium = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ TextStacking = TileTextStacking.Center,
+
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "Hi,",
+ HintStyle = AdaptiveTextStyle.Base,
+ HintAlign = AdaptiveTextAlign.Center
+ },
+
+ new AdaptiveText()
+ {
+ Text = "MasterHip",
+ HintStyle = AdaptiveTextStyle.CaptionSubtle,
+ HintAlign = AdaptiveTextAlign.Center
+ }
+ }
+ }
+ };
+
+ TileBinding wide = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ Children =
+ {
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup()
+ {
+ HintWeight = 33,
+ Children =
+ {
+ new AdaptiveImage()
+ {
+ Source = "http://xbox.com/MasterHip/profile.jpg",
+ HintCrop = AdaptiveImageCrop.Circle
+ }
+ }
+ },
+
+ new AdaptiveSubgroup()
+ {
+ HintTextStacking = AdaptiveSubgroupTextStacking.Center,
+ Children =
+ {
+ new AdaptiveText()
+ {
+ Text = "Hi,",
+ HintStyle = AdaptiveTextStyle.Title
+ },
+
+ new AdaptiveText()
+ {
+ Text = "MasterHip",
+ HintStyle = AdaptiveTextStyle.SubtitleSubtle
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ };
+
+ TileBinding large = new TileBinding()
+ {
+ Content = new TileBindingContentAdaptive()
+ {
+ TextStacking = TileTextStacking.Center,
+ Children =
+ {
+ new AdaptiveGroup()
+ {
+ Children =
+ {
+ new AdaptiveSubgroup() { HintWeight = 1 },
+ new AdaptiveSubgroup()
+ {
+ HintWeight = 2,
+ Children =
+ {
+ new AdaptiveImage()
+ {
+ Source = "http://xbox.com/MasterHip/profile.jpg",
+ HintCrop = AdaptiveImageCrop.Circle
+ }
+ }
+ },
+ new AdaptiveSubgroup() { HintWeight = 1 }
+ }
+ },
+
+ new AdaptiveText()
+ {
+ Text = "Hi,",
+ HintStyle = AdaptiveTextStyle.Title,
+ HintAlign = AdaptiveTextAlign.Center
+ },
+
+ new AdaptiveText()
+ {
+ Text = "MasterHip",
+ HintStyle = AdaptiveTextStyle.SubtitleSubtle,
+ HintAlign = AdaptiveTextAlign.Center
+ }
+ }
+ }
+ };
+
+ TileContent content = new TileContent()
+ {
+ Visual = new TileVisual()
+ {
+ Branding = TileBranding.NameAndLogo,
+
+ TileMedium = medium,
+ TileWide = wide,
+ TileLarge = large
+ }
+ };
+
+ string expectedXml = $@"";
+
+ // Medium
+ expectedXml += @"Hi, MasterHip ";
+
+ // Wide
+ expectedXml += @"Hi, MasterHip ";
+
+ // Large
+ expectedXml += @"Hi, MasterHip ";
+
+ expectedXml += " ";
+
+ AssertHelper.AssertTile(expectedXml, content);
+ }
+ }
+}
+#endif