From 4c9e111e6191c22bbe2b829ce2b393696c8a7101 Mon Sep 17 00:00:00 2001 From: Alvin Ashcraft Date: Sat, 29 Jul 2023 14:58:47 -0400 Subject: [PATCH] Update completed solution for chapter 8 --- .../Complete/MyMediaCollection/App.xaml.cs | 63 ++++++++++++- .../Helpers/NotificationManager.cs | 88 +++++++++++++++++++ .../Helpers/NotificationShared.cs | 43 +++++++++ .../Helpers/ToastWithAvatar.cs | 42 +++++++++ .../Helpers/ToastWithText.cs | 47 ++++++++++ .../MyMediaCollection.csproj | 8 +- .../MyMediaCollection/Package.appxmanifest | 14 +++ .../ViewModels/MainViewModel.cs | 19 ++++ .../MyMediaCollection/Views/MainPage.xaml | 8 ++ .../MyMediaCollection/Views/MainPage.xaml.cs | 62 +++++++++++++ 10 files changed, 387 insertions(+), 7 deletions(-) create mode 100644 Chapter08/Complete/MyMediaCollection/Helpers/NotificationManager.cs create mode 100644 Chapter08/Complete/MyMediaCollection/Helpers/NotificationShared.cs create mode 100644 Chapter08/Complete/MyMediaCollection/Helpers/ToastWithAvatar.cs create mode 100644 Chapter08/Complete/MyMediaCollection/Helpers/ToastWithText.cs diff --git a/Chapter08/Complete/MyMediaCollection/App.xaml.cs b/Chapter08/Complete/MyMediaCollection/App.xaml.cs index a7faf93..9a2ce58 100644 --- a/Chapter08/Complete/MyMediaCollection/App.xaml.cs +++ b/Chapter08/Complete/MyMediaCollection/App.xaml.cs @@ -3,13 +3,17 @@ using Microsoft.Extensions.Hosting; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Navigation; +using Microsoft.Windows.AppLifecycle; +using Microsoft.Windows.AppNotifications; +using MyMediaCollection.Helpers; using MyMediaCollection.Interfaces; using MyMediaCollection.Services; using MyMediaCollection.ViewModels; using MyMediaCollection.Views; using System; -using System.Net; +using System.Runtime.InteropServices; using System.Threading.Tasks; +using WinRT.Interop; namespace MyMediaCollection { @@ -18,6 +22,11 @@ namespace MyMediaCollection /// public partial class App : Application { + [DllImport("user32.dll", SetLastError = true)] + static extern void SwitchToThisWindow(IntPtr hWnd, bool turnOn); + + private NotificationManager notificationManager; + public static IHost HostContainer { get; private set; } /// @@ -27,13 +36,45 @@ namespace MyMediaCollection public App() { this.InitializeComponent(); + notificationManager = new NotificationManager(); + notificationManager.Init(); + AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit; + } + + private void CurrentDomain_ProcessExit(object sender, EventArgs e) + { + notificationManager.Unregister(); + } + + public static void ToForeground() + { + if (m_window != null) + { + IntPtr handle = WindowNative.GetWindowHandle(m_window); + if (handle != IntPtr.Zero) + { + SwitchToThisWindow(handle, true); + } + } + } + + public static string GetFullPathToExe() + { + var path = AppDomain.CurrentDomain.BaseDirectory; + var pos = path.LastIndexOf("\\"); + return path.Substring(0, pos); + } + + public static string GetFullPathToAsset(string assetName) + { + return $"{GetFullPathToExe()}\\Assets\\{assetName}"; } /// /// Invoked when the application is launched. /// /// Details about the launch request and process. - protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + protected override async void OnLaunched(LaunchActivatedEventArgs args) { m_window = new MainWindow(); var rootFrame = new Frame(); @@ -41,6 +82,22 @@ namespace MyMediaCollection rootFrame.NavigationFailed += RootFrame_NavigationFailed; rootFrame.Navigate(typeof(MainPage), args); m_window.Content = rootFrame; + + var currentInstance = AppInstance.GetCurrent(); + if (currentInstance.IsCurrent) + { + AppActivationArguments activationArgs = currentInstance.GetActivatedEventArgs(); + if (activationArgs != null) + { + ExtendedActivationKind extendedKind = activationArgs.Kind; + if (extendedKind == ExtendedActivationKind.AppNotification) + { + var notificationActivatedEventArgs = (AppNotificationActivatedEventArgs)activationArgs.Data; + notificationManager.ProcessLaunchActivationArgs(notificationActivatedEventArgs); + } + } + } + m_window.Activate(); } @@ -49,7 +106,7 @@ namespace MyMediaCollection throw new Exception($"Error loading page {e.SourcePageType.FullName}"); } - private Window m_window; + private static Window m_window; internal Window Window => m_window; diff --git a/Chapter08/Complete/MyMediaCollection/Helpers/NotificationManager.cs b/Chapter08/Complete/MyMediaCollection/Helpers/NotificationManager.cs new file mode 100644 index 0000000..a9fa398 --- /dev/null +++ b/Chapter08/Complete/MyMediaCollection/Helpers/NotificationManager.cs @@ -0,0 +1,88 @@ +using Microsoft.Windows.AppNotifications; +using System; +using System.Collections.Generic; + +namespace MyMediaCollection.Helpers +{ + internal class NotificationManager + { + private bool isRegistered; + private Dictionary> notificationHandlers; + + public NotificationManager() + { + isRegistered = false; + + // When adding new a scenario, be sure to add its notification handler here. + notificationHandlers = new Dictionary> + { + { ToastWithAvatar.ScenarioId, ToastWithAvatar.NotificationReceived }, + { ToastWithText.ScenarioId, ToastWithText.NotificationReceived } + }; + } + + ~NotificationManager() + { + Unregister(); + } + + public void Unregister() + { + if (isRegistered) + { + AppNotificationManager.Default.Unregister(); + isRegistered = false; + } + } + + public void Init() + { + AppNotificationManager notificationManager = AppNotificationManager.Default; + + // Add handler before calling Register. + notificationManager.NotificationInvoked += OnNotificationInvoked; + notificationManager.Register(); + + isRegistered = true; + } + + public void ProcessLaunchActivationArgs(AppNotificationActivatedEventArgs notificationActivatedEventArgs) + { + DispatchNotification(notificationActivatedEventArgs); + NotificationShared.AppLaunchedFromNotification(); + } + + private bool DispatchNotification(AppNotificationActivatedEventArgs notificationActivatedEventArgs) + { + var scenarioId = notificationActivatedEventArgs.Arguments[NotificationShared.scenarioTag]; + if (scenarioId.Length != 0) + { + try + { + notificationHandlers[int.Parse(scenarioId)](notificationActivatedEventArgs); + return true; + } + catch + { + // No matching NotificationHandler for scenarioId. + return false; + } + } + else + { + // No scenarioId provided + return false; + } + } + + public void OnNotificationInvoked(object sender, AppNotificationActivatedEventArgs notificationActivatedEventArgs) + { + NotificationShared.NotificationReceived(); + + if (!DispatchNotification(notificationActivatedEventArgs)) + { + NotificationShared.UnrecognizedToastOriginator(); + } + } + } +} \ No newline at end of file diff --git a/Chapter08/Complete/MyMediaCollection/Helpers/NotificationShared.cs b/Chapter08/Complete/MyMediaCollection/Helpers/NotificationShared.cs new file mode 100644 index 0000000..8fad23a --- /dev/null +++ b/Chapter08/Complete/MyMediaCollection/Helpers/NotificationShared.cs @@ -0,0 +1,43 @@ +using Microsoft.UI.Xaml.Controls; +using MyMediaCollection.Views; + +namespace MyMediaCollection.Helpers +{ + public class NotificationShared + { + public const string scenarioTag = "scenarioId"; + + public struct Notification + { + public string Originator; + public string Action; + public bool HasInput; + public string Input; + }; + + public static void CouldNotSendToast() + { + MainPage.Current.NotifyUser("Could not send toast", InfoBarSeverity.Error); + } + + public static void ToastSentSuccessfully() + { + MainPage.Current.NotifyUser("Toast sent successfully!", InfoBarSeverity.Success); + } + + public static void AppLaunchedFromNotification() + { + MainPage.Current.NotifyUser("App launched from notifications", InfoBarSeverity.Informational); + } + + public static void NotificationReceived() + { + MainPage.Current.NotifyUser("Notification received", InfoBarSeverity.Informational); + } + + public static void UnrecognizedToastOriginator() + { + MainPage.Current.NotifyUser("Unrecognized Toast Originator or Unknown Error", InfoBarSeverity.Error); + } + } +} \ No newline at end of file diff --git a/Chapter08/Complete/MyMediaCollection/Helpers/ToastWithAvatar.cs b/Chapter08/Complete/MyMediaCollection/Helpers/ToastWithAvatar.cs new file mode 100644 index 0000000..049946b --- /dev/null +++ b/Chapter08/Complete/MyMediaCollection/Helpers/ToastWithAvatar.cs @@ -0,0 +1,42 @@ +using Microsoft.Windows.AppNotifications.Builder; +using Microsoft.Windows.AppNotifications; +using MyMediaCollection.Views; + +namespace MyMediaCollection.Helpers +{ + public class ToastWithAvatar + { + public const int ScenarioId = 1; + public const string ScenarioName = "Local Toast with Image"; + + public static bool SendToast() + { + var appNotification = new AppNotificationBuilder() + .AddArgument("action", "ToastClick") + .AddArgument(NotificationShared.scenarioTag, ScenarioId.ToString()) + .SetAppLogoOverride(new System.Uri($"file://{App.GetFullPathToAsset("Square150x150Logo.scale-200.png")}"), AppNotificationImageCrop.Circle) + .AddText(ScenarioName) + .AddText("This is a notification message.") + .AddButton(new AppNotificationButton("Open App") + .AddArgument("action", "OpenApp") + .AddArgument(NotificationShared.scenarioTag, ScenarioId.ToString())) + .BuildNotification(); + + AppNotificationManager.Default.Show(appNotification); + + // If notification is sent, it will have an Id. Success. + return appNotification.Id != 0; + } + + public static void NotificationReceived(AppNotificationActivatedEventArgs notificationActivatedEventArgs) + { + var notification = new NotificationShared.Notification + { + Originator = ScenarioName, + Action = notificationActivatedEventArgs.Arguments["action"] + }; + MainPage.Current.NotificationReceived(notification); + App.ToForeground(); + } + } +} \ No newline at end of file diff --git a/Chapter08/Complete/MyMediaCollection/Helpers/ToastWithText.cs b/Chapter08/Complete/MyMediaCollection/Helpers/ToastWithText.cs new file mode 100644 index 0000000..e383298 --- /dev/null +++ b/Chapter08/Complete/MyMediaCollection/Helpers/ToastWithText.cs @@ -0,0 +1,47 @@ +using Microsoft.Windows.AppNotifications.Builder; +using Microsoft.Windows.AppNotifications; +using MyMediaCollection.Views; + +namespace MyMediaCollection.Helpers +{ + public class ToastWithText + { + public const int ScenarioId = 2; + public const string ScenarioName = "Local Toast with Image and Text Entry"; + const string textboxReplyId = "textboxReply"; + + public static bool SendToast() + { + var appNotification = new AppNotificationBuilder() + .AddArgument("action", "ToastClick") + .AddArgument(NotificationShared.scenarioTag, ScenarioId.ToString()) + .SetAppLogoOverride(new System.Uri($"file://{App.GetFullPathToAsset("Square150x150Logo.scale-200.png")}"), AppNotificationImageCrop.Circle) + .AddText(ScenarioName) + .AddText("This is a notification message.") + .AddTextBox(textboxReplyId, "Enter a reply", "Reply box") + .AddButton(new AppNotificationButton("Reply") + .AddArgument("action", "Reply") + .AddArgument(NotificationShared.scenarioTag, ScenarioId.ToString()) + .SetInputId(textboxReplyId)) + .BuildNotification(); + + AppNotificationManager.Default.Show(appNotification); + + // If notification is sent, it will have an Id. Success. + return appNotification.Id != 0; + } + + public static void NotificationReceived(AppNotificationActivatedEventArgs notificationActivatedEventArgs) + { + var notification = new NotificationShared.Notification + { + Originator = ScenarioName, + Action = notificationActivatedEventArgs.Arguments["action"], + HasInput = true, + Input = notificationActivatedEventArgs.UserInput[textboxReplyId] + }; + MainPage.Current.NotificationReceived(notification); + App.ToForeground(); + } + } +} \ No newline at end of file diff --git a/Chapter08/Complete/MyMediaCollection/MyMediaCollection.csproj b/Chapter08/Complete/MyMediaCollection/MyMediaCollection.csproj index 476f29f..6d42fbf 100644 --- a/Chapter08/Complete/MyMediaCollection/MyMediaCollection.csproj +++ b/Chapter08/Complete/MyMediaCollection/MyMediaCollection.csproj @@ -28,13 +28,13 @@ - + - + - - + + diff --git a/Chapter08/Complete/MyMediaCollection/Package.appxmanifest b/Chapter08/Complete/MyMediaCollection/Package.appxmanifest index f15a1ea..6faa5e1 100644 --- a/Chapter08/Complete/MyMediaCollection/Package.appxmanifest +++ b/Chapter08/Complete/MyMediaCollection/Package.appxmanifest @@ -4,6 +4,8 @@ xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities" + xmlns:com="http://schemas.microsoft.com/appx/manifest/com/windows10" + xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" IgnorableNamespaces="uap rescap"> + + + + + + + + + + + + diff --git a/Chapter08/Complete/MyMediaCollection/ViewModels/MainViewModel.cs b/Chapter08/Complete/MyMediaCollection/ViewModels/MainViewModel.cs index 3def498..d718f18 100644 --- a/Chapter08/Complete/MyMediaCollection/ViewModels/MainViewModel.cs +++ b/Chapter08/Complete/MyMediaCollection/ViewModels/MainViewModel.cs @@ -1,6 +1,7 @@ using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.UI.Xaml.Input; +using MyMediaCollection.Helpers; using MyMediaCollection.Interfaces; using MyMediaCollection.Model; using System.Collections.ObjectModel; @@ -98,5 +99,23 @@ namespace MyMediaCollection.ViewModels } private bool CanDeleteItem() => SelectedMediaItem != null; + + [RelayCommand] + private void SendToast() + { + if (ToastWithAvatar.SendToast()) + NotificationShared.ToastSentSuccessfully(); + else + NotificationShared.CouldNotSendToast(); + } + + [RelayCommand] + private void SendToastWithText() + { + if (ToastWithText.SendToast()) + NotificationShared.ToastSentSuccessfully(); + else + NotificationShared.CouldNotSendToast(); + } } } \ No newline at end of file diff --git a/Chapter08/Complete/MyMediaCollection/Views/MainPage.xaml b/Chapter08/Complete/MyMediaCollection/Views/MainPage.xaml index ad422cf..8c79b8c 100644 --- a/Chapter08/Complete/MyMediaCollection/Views/MainPage.xaml +++ b/Chapter08/Complete/MyMediaCollection/Views/MainPage.xaml @@ -14,6 +14,7 @@ + @@ -69,6 +70,12 @@ Margin="4,0"> + public sealed partial class MainPage : Page { + public static MainPage Current; + public MainPage() { ViewModel = App.HostContainer.Services.GetService(); this.InitializeComponent(); + Current = this; Loaded += MainPage_Loaded; } @@ -27,5 +31,63 @@ namespace MyMediaCollection.Views } public MainViewModel ViewModel; + + public void NotifyUser(string message, InfoBarSeverity severity, bool isOpen = true) + { + if (DispatcherQueue.HasThreadAccess) + { + UpdateStatus(message, severity, isOpen); + } + else + { + DispatcherQueue.TryEnqueue(() => + { + UpdateStatus(message, severity, isOpen); + }); + } + } + + private void UpdateStatus(string message, InfoBarSeverity severity, bool isOpen) + { + notifyInfoBar.Message = message; + notifyInfoBar.IsOpen = isOpen; + notifyInfoBar.Severity = severity; + } + + public void NotificationReceived(NotificationShared.Notification notification) + { + var text = $"{notification.Originator}; Action: {notification.Action}"; + + if (notification.HasInput) + { + if (string.IsNullOrWhiteSpace(notification.Input)) + text += "; No input received"; + else + text += $"; Input received: {notification.Input}"; + } + + if (DispatcherQueue.HasThreadAccess) + DisplayMessageDialog(text); + else + { + DispatcherQueue.TryEnqueue(() => + { + DisplayMessageDialog(text); + }); + } + } + + private void DisplayMessageDialog(string message) + { + ContentDialog notifyDialog = new() + { + XamlRoot = this.XamlRoot, + Title = "Notification received", + Content = message, + CloseButtonText = "Ok" + }; + + notifyDialog.ShowAsync(); + } } } \ No newline at end of file