diff --git a/Chapter11/MyMediaCollection/Ch11-MyMediaCollection.sln b/Chapter11/MyMediaCollection/Ch11-MyMediaCollection.sln
new file mode 100644
index 0000000..d74113c
--- /dev/null
+++ b/Chapter11/MyMediaCollection/Ch11-MyMediaCollection.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.5.33530.505
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MyMediaCollection", "MyMediaCollection\MyMediaCollection.csproj", "{972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|ARM64.Build.0 = Debug|ARM64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|x64.ActiveCfg = Debug|x64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|x64.Build.0 = Debug|x64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|x64.Deploy.0 = Debug|x64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|x86.ActiveCfg = Debug|x86
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|x86.Build.0 = Debug|x86
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Debug|x86.Deploy.0 = Debug|x86
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|ARM64.ActiveCfg = Release|ARM64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|ARM64.Build.0 = Release|ARM64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|ARM64.Deploy.0 = Release|ARM64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|x64.ActiveCfg = Release|x64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|x64.Build.0 = Release|x64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|x64.Deploy.0 = Release|x64
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|x86.ActiveCfg = Release|x86
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|x86.Build.0 = Release|x86
+ {972D5C0D-86E6-4A2F-A6FD-8D4FE3380707}.Release|x86.Deploy.0 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {C9277197-C949-4948-80CD-0A685EE6DCBB}
+ EndGlobalSection
+EndGlobal
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/App.xaml b/Chapter11/MyMediaCollection/MyMediaCollection/App.xaml
new file mode 100644
index 0000000..eeb9e6e
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/App.xaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/App.xaml.cs b/Chapter11/MyMediaCollection/MyMediaCollection/App.xaml.cs
new file mode 100644
index 0000000..9a2ce58
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/App.xaml.cs
@@ -0,0 +1,131 @@
+using Microsoft.Extensions.DependencyInjection;
+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.Runtime.InteropServices;
+using System.Threading.Tasks;
+using WinRT.Interop;
+
+namespace MyMediaCollection
+{
+ ///
+ /// Provides application-specific behavior to supplement the default Application class.
+ ///
+ 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; }
+
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ 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(LaunchActivatedEventArgs args)
+ {
+ m_window = new MainWindow();
+ var rootFrame = new Frame();
+ await RegisterComponentsAsync(rootFrame);
+ 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();
+ }
+
+ private void RootFrame_NavigationFailed(object sender, NavigationFailedEventArgs e)
+ {
+ throw new Exception($"Error loading page {e.SourcePageType.FullName}");
+ }
+
+ private static Window m_window;
+
+ internal Window Window => m_window;
+
+ private async Task RegisterComponentsAsync(Frame rootFrame)
+ {
+ var navigationService = new NavigationService(rootFrame);
+ navigationService.Configure(nameof(MainPage), typeof(MainPage));
+ navigationService.Configure(nameof(ItemDetailsPage), typeof(ItemDetailsPage));
+ var dataService = new SqliteDataService();
+ await dataService.InitializeDataAsync();
+
+ HostContainer = Host.CreateDefaultBuilder()
+ .ConfigureServices(services =>
+ {
+ services.AddSingleton(navigationService);
+ services.AddSingleton(dataService);
+ services.AddTransient();
+ services.AddTransient();
+ }).Build();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Assets/LockScreenLogo.scale-200.png b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/LockScreenLogo.scale-200.png
new file mode 100644
index 0000000..7440f0d
Binary files /dev/null and b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/LockScreenLogo.scale-200.png differ
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Assets/SplashScreen.scale-200.png b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/SplashScreen.scale-200.png
new file mode 100644
index 0000000..32f486a
Binary files /dev/null and b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/SplashScreen.scale-200.png differ
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square150x150Logo.scale-200.png b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000..53ee377
Binary files /dev/null and b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square150x150Logo.scale-200.png differ
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square44x44Logo.scale-200.png b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000..f713bba
Binary files /dev/null and b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square44x44Logo.scale-200.png differ
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000..dc9f5be
Binary files /dev/null and b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Assets/StoreLogo.png b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/StoreLogo.png
new file mode 100644
index 0000000..a4586f2
Binary files /dev/null and b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/StoreLogo.png differ
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Wide310x150Logo.scale-200.png b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000..8b4a5d0
Binary files /dev/null and b/Chapter11/MyMediaCollection/MyMediaCollection/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Enums/ItemType.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Enums/ItemType.cs
new file mode 100644
index 0000000..2e50873
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Enums/ItemType.cs
@@ -0,0 +1,9 @@
+namespace MyMediaCollection.Enums
+{
+ public enum ItemType
+ {
+ Music,
+ Video,
+ Book
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Enums/LocationType.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Enums/LocationType.cs
new file mode 100644
index 0000000..a8d19aa
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Enums/LocationType.cs
@@ -0,0 +1,8 @@
+namespace MyMediaCollection.Enums
+{
+ public enum LocationType
+ {
+ InCollection,
+ Loaned
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/NotificationManager.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/NotificationManager.cs
new file mode 100644
index 0000000..a9fa398
--- /dev/null
+++ b/Chapter11/MyMediaCollection/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/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/NotificationShared.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/NotificationShared.cs
new file mode 100644
index 0000000..8fad23a
--- /dev/null
+++ b/Chapter11/MyMediaCollection/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/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/ToastWithAvatar.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/ToastWithAvatar.cs
new file mode 100644
index 0000000..049946b
--- /dev/null
+++ b/Chapter11/MyMediaCollection/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/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/ToastWithText.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Helpers/ToastWithText.cs
new file mode 100644
index 0000000..e383298
--- /dev/null
+++ b/Chapter11/MyMediaCollection/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/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/IDataService.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/IDataService.cs
new file mode 100644
index 0000000..7d6721b
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/IDataService.cs
@@ -0,0 +1,22 @@
+using MyMediaCollection.Enums;
+using MyMediaCollection.Model;
+using System.Collections.Generic;
+using System.Threading.Tasks;
+
+namespace MyMediaCollection.Interfaces
+{
+ public interface IDataService
+ {
+ Task> GetItemsAsync();
+ Task GetItemAsync(int id);
+ Task AddItemAsync(MediaItem item);
+ Task UpdateItemAsync(MediaItem item);
+ Task DeleteItemAsync(MediaItem item);
+ IList GetItemTypes();
+ Medium GetMedium(string name);
+ IList GetMediums();
+ IList GetMediums(ItemType itemType);
+ IList GetLocationTypes();
+ Task InitializeDataAsync();
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/INavigationService.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/INavigationService.cs
new file mode 100644
index 0000000..c8ccee6
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/INavigationService.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace MyMediaCollection.Interfaces
+{
+ public interface INavigationService
+ {
+ string CurrentPage { get; }
+ void NavigateTo(string page);
+ void NavigateTo(string page, object parameter);
+ void GoBack();
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/IValidatable.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/IValidatable.cs
new file mode 100644
index 0000000..95cebf4
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Interfaces/IValidatable.cs
@@ -0,0 +1,7 @@
+namespace MyMediaCollection.Interfaces
+{
+ public interface IValidatable
+ {
+ void Validate(string memberName, object value);
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/MainWindow.xaml b/Chapter11/MyMediaCollection/MyMediaCollection/MainWindow.xaml
new file mode 100644
index 0000000..7ebe85e
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/MainWindow.xaml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/MainWindow.xaml.cs b/Chapter11/MyMediaCollection/MyMediaCollection/MainWindow.xaml.cs
new file mode 100644
index 0000000..6da1b61
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/MainWindow.xaml.cs
@@ -0,0 +1,47 @@
+using Microsoft.UI;
+using Microsoft.UI.Composition.SystemBackdrops;
+using Microsoft.UI.Windowing;
+using Microsoft.UI.Xaml;
+using System;
+using WinRT.Interop;
+using Microsoft.UI.Xaml.Media;
+
+namespace MyMediaCollection
+{
+ ///
+ /// An empty window that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class MainWindow : Window
+ {
+ private AppWindow _appWindow;
+ private const string AppTitle = "My Media Collection";
+
+ public MainWindow()
+ {
+ this.InitializeComponent();
+ SystemBackdrop = new MicaBackdrop
+ {
+ Kind = MicaKind.BaseAlt
+ };
+ _appWindow = GetCurrentAppWindow();
+ _appWindow.Title = AppTitle;
+ }
+
+ private AppWindow GetCurrentAppWindow()
+ {
+ IntPtr handle = WindowNative.GetWindowHandle(this);
+ WindowId windowId = Win32Interop.GetWindowIdFromWindow(handle);
+ return AppWindow.GetFromWindowId(windowId);
+ }
+
+ internal void SetPageTitle(string title)
+ {
+ if (_appWindow == null)
+ {
+ _appWindow = GetCurrentAppWindow();
+ }
+
+ _appWindow.Title = $"{AppTitle} - {title}";
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Model/MediaItem.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Model/MediaItem.cs
new file mode 100644
index 0000000..329af7f
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Model/MediaItem.cs
@@ -0,0 +1,17 @@
+using Dapper.Contrib.Extensions;
+using MyMediaCollection.Enums;
+
+namespace MyMediaCollection.Model
+{
+ public class MediaItem
+ {
+ [Key]
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public ItemType MediaType { get; set; }
+ [Computed]
+ public Medium MediumInfo { get; set; }
+ public LocationType Location { get; set; }
+ public int MediumId => MediumInfo.Id;
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Model/Medium.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Model/Medium.cs
new file mode 100644
index 0000000..3c23205
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Model/Medium.cs
@@ -0,0 +1,13 @@
+using Dapper.Contrib.Extensions;
+using MyMediaCollection.Enums;
+
+namespace MyMediaCollection.Model
+{
+ public class Medium
+ {
+ [Key]
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public ItemType MediaType { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/MyMediaCollection.csproj b/Chapter11/MyMediaCollection/MyMediaCollection/MyMediaCollection.csproj
new file mode 100644
index 0000000..6d42fbf
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/MyMediaCollection.csproj
@@ -0,0 +1,68 @@
+
+
+ WinExe
+ net6.0-windows10.0.19041.0
+ 10.0.17763.0
+ MyMediaCollection
+ app.manifest
+ x86;x64;ARM64
+ win10-x86;win10-x64;win10-arm64
+ win10-$(Platform).pubxml
+ true
+ true
+ 10.0.18362.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+ MSBuild:Compile
+
+
+
+
+
+ true
+
+
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Package.appxmanifest b/Chapter11/MyMediaCollection/MyMediaCollection/Package.appxmanifest
new file mode 100644
index 0000000..6faa5e1
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Package.appxmanifest
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+ MyMediaCollection
+ alash
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Properties/launchSettings.json b/Chapter11/MyMediaCollection/MyMediaCollection/Properties/launchSettings.json
new file mode 100644
index 0000000..b7ae329
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "MyMediaCollection (Package)": {
+ "commandName": "MsixPackage"
+ },
+ "MyMediaCollection (Unpackaged)": {
+ "commandName": "Project"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Services/DataService.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Services/DataService.cs
new file mode 100644
index 0000000..a81fcab
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Services/DataService.cs
@@ -0,0 +1,175 @@
+using MyMediaCollection.Enums;
+using MyMediaCollection.Interfaces;
+using MyMediaCollection.Model;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MyMediaCollection.Services
+{
+ public class DataService : IDataService
+ {
+ private IList _items;
+ private IList _itemTypes;
+ private IList _mediums;
+ private IList _locationTypes;
+
+ public async Task InitializeDataAsync()
+ {
+ PopulateItemTypes();
+ PopulateMediums();
+ PopulateLocationTypes();
+ PopulateItems();
+ await Task.Delay(1);
+ }
+
+ private void PopulateItems()
+ {
+ var cd = new MediaItem
+ {
+ Id = 1,
+ Name = "Classical Favorites",
+ MediaType = ItemType.Music,
+ MediumInfo = _mediums.FirstOrDefault(m => m.Name == "CD"),
+ Location = LocationType.InCollection
+ };
+
+ var book = new MediaItem
+ {
+ Id = 2,
+ Name = "Classic Fairy Tales",
+ MediaType = ItemType.Book,
+ MediumInfo = _mediums.FirstOrDefault(m => m.Name == "Hardcover"),
+ Location = LocationType.InCollection
+ };
+
+ var bluRay = new MediaItem
+ {
+ Id = 3,
+ Name = "The Mummy",
+ MediaType = ItemType.Video,
+ MediumInfo = _mediums.FirstOrDefault(m => m.Name == "Blu Ray"),
+ Location = LocationType.InCollection
+ };
+
+ _items = new List
+ {
+ cd,
+ book,
+ bluRay
+ };
+ }
+
+ private void PopulateMediums()
+ {
+ var cd = new Medium { Id = 1, MediaType = ItemType.Music, Name = "CD" };
+ var vinyl = new Medium { Id = 2, MediaType = ItemType.Music, Name = "Vinyl" };
+ var hardcover = new Medium { Id = 3, MediaType = ItemType.Book, Name = "Hardcover" };
+ var paperback = new Medium { Id = 4, MediaType = ItemType.Book, Name = "Paperback" };
+ var dvd = new Medium { Id = 5, MediaType = ItemType.Video, Name = "DVD" };
+ var bluRay = new Medium { Id = 6, MediaType = ItemType.Video, Name = "Blu Ray" };
+
+ _mediums = new List
+ {
+ cd,
+ vinyl,
+ hardcover,
+ paperback,
+ dvd,
+ bluRay
+ };
+ }
+
+ private void PopulateItemTypes()
+ {
+ _itemTypes = new List
+ {
+ ItemType.Book,
+ ItemType.Music,
+ ItemType.Video
+ };
+ }
+
+ private void PopulateLocationTypes()
+ {
+ _locationTypes = new List
+ {
+ LocationType.InCollection,
+ LocationType.Loaned
+ };
+ }
+
+ public async Task AddItemAsync(MediaItem item)
+ {
+ item.Id = _items.Max(i => i.Id) + 1;
+ _items.Add(item);
+ await Task.Delay(1);
+
+ return item.Id;
+ }
+
+ public async Task GetItemAsync(int id)
+ {
+ await Task.Delay(1);
+ return _items.FirstOrDefault(i => i.Id == id);
+ }
+
+ public async Task> GetItemsAsync()
+ {
+ await Task.Delay(1);
+ return _items;
+ }
+
+ public IList GetItemTypes()
+ {
+ return _itemTypes;
+ }
+
+ public IList GetMediums()
+ {
+ return _mediums;
+ }
+
+ public IList GetMediums(ItemType itemType)
+ {
+ return _mediums
+ .Where(m => m.MediaType == itemType)
+ .ToList();
+ }
+
+ public IList GetLocationTypes()
+ {
+ return _locationTypes;
+ }
+
+ public async Task UpdateItemAsync(MediaItem item)
+ {
+ var idx = -1;
+ var matchedItem =
+ (from x in _items
+ let ind = idx++
+ where x.Id == item.Id
+ select ind).FirstOrDefault();
+
+ if (idx == -1)
+ {
+ throw new Exception("Unable to update item. Item not found in collection.");
+ }
+
+ _items[idx] = item;
+ await Task.Delay(1);
+ }
+
+ public async Task DeleteItemAsync(MediaItem item)
+ {
+ await Task.Delay(1);
+ throw new NotImplementedException();
+ }
+
+ public Medium GetMedium(string name)
+ {
+ return _mediums.FirstOrDefault(m => m.Name == name);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Services/NavigationService.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Services/NavigationService.cs
new file mode 100644
index 0000000..63f8c95
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Services/NavigationService.cs
@@ -0,0 +1,84 @@
+using Microsoft.UI.Xaml.Controls;
+using MyMediaCollection.Interfaces;
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace MyMediaCollection.Services
+{
+ public class NavigationService : INavigationService
+ {
+ public NavigationService(Frame rootFrame)
+ {
+ AppFrame = rootFrame;
+ }
+
+ private readonly IDictionary _pages = new ConcurrentDictionary();
+
+ public const string RootPage = "(Root)";
+
+ public const string UnknownPage = "(Unknown)";
+
+ private static Frame AppFrame;
+
+ public void Configure(string page, Type type)
+ {
+ if (_pages.Values.Any(v => v == type))
+ {
+ throw new ArgumentException($"The {type.Name} view has already been registered under another name.");
+ }
+
+ _pages[page] = type;
+ }
+
+ ///
+ /// Gets the name of the currently displayed page.
+ ///
+ public string CurrentPage
+ {
+ get
+ {
+ var frame = AppFrame;
+
+ if (frame.BackStackDepth == 0)
+ return RootPage;
+
+ if (frame.Content == null)
+ return UnknownPage;
+
+ var type = frame.Content.GetType();
+
+ if (_pages.Values.All(v => v != type))
+ return UnknownPage;
+
+ var item = _pages.Single(i => i.Value == type);
+
+ return item.Key;
+ }
+ }
+
+ public void NavigateTo(string page)
+ {
+ NavigateTo(page, null);
+ }
+
+ public void NavigateTo(string page, object parameter)
+ {
+ if (!_pages.ContainsKey(page))
+ {
+ throw new ArgumentException($"Unable to find a page registered with the name {page}.");
+ }
+
+ AppFrame.Navigate(_pages[page], parameter);
+ }
+
+ public void GoBack()
+ {
+ if (AppFrame?.CanGoBack == true)
+ {
+ AppFrame.GoBack();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Services/SqliteDataService.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Services/SqliteDataService.cs
new file mode 100644
index 0000000..5a88079
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Services/SqliteDataService.cs
@@ -0,0 +1,300 @@
+using Dapper;
+using Dapper.Contrib.Extensions;
+using Microsoft.Data.Sqlite;
+using MyMediaCollection.Enums;
+using MyMediaCollection.Interfaces;
+using MyMediaCollection.Model;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Windows.Storage;
+
+namespace MyMediaCollection.Services
+{
+ public class SqliteDataService : IDataService
+ {
+ private IList _itemTypes;
+ private IList _mediums;
+ private IList _locationTypes;
+ private const string DbName = "mediaCollectionData.db";
+
+ private async Task GetOpenConnectionAsync()
+ {
+ await ApplicationData.Current.LocalFolder.CreateFileAsync(DbName, CreationCollisionOption.OpenIfExists).AsTask();
+
+ string dbPath = Path.Combine(ApplicationData.Current.LocalFolder.Path, DbName);
+ var cn = new SqliteConnection($"Filename={dbPath}");
+ cn.Open();
+
+ return cn;
+ }
+
+ private async Task CreateMediumTableAsync(SqliteConnection db)
+ {
+ string tableCommand = @"CREATE TABLE IF NOT
+ EXISTS Mediums (Id INTEGER PRIMARY KEY AUTOINCREMENT,
+ Name NVARCHAR(30) NOT NULL,
+ MediumType INTEGER NOT NULL)";
+
+ using var createTable = new SqliteCommand(tableCommand, db);
+
+ await createTable.ExecuteNonQueryAsync();
+ }
+
+ private async Task CreateMediaItemTableAsync(SqliteConnection db)
+ {
+ string tableCommand = @"CREATE TABLE IF NOT
+ EXISTS MediaItems (Id INTEGER PRIMARY KEY AUTOINCREMENT,
+ Name NVARCHAR(1000) NOT NULL,
+ ItemType INTEGER NOT NULL,
+ MediumId INTEGER NOT NULL,
+ LocationType INTEGER,
+ CONSTRAINT fk_mediums
+ FOREIGN KEY(MediumId)
+ REFERENCES Mediums(Id))";
+
+ using var createTable = new SqliteCommand(tableCommand, db);
+
+ await createTable.ExecuteNonQueryAsync();
+ }
+
+ private async Task InsertMediumAsync(SqliteConnection db, Medium medium)
+ {
+ //// IMPLEMENTATION WITHOUT DAPPER
+ //using var insertCommand = new SqliteCommand
+ //{
+ // Connection = db,
+ // CommandText = "INSERT INTO Mediums VALUES (NULL, @Name, @MediumType);"
+ //};
+ //insertCommand.Parameters.AddWithValue("@Name", medium.Name);
+ //insertCommand.Parameters.AddWithValue("@MediumType", (int)medium.MediaType);
+ //await insertCommand.ExecuteNonQueryAsync();
+
+ var newIds = await db.QueryAsync(
+ $@"INSERT INTO Mediums
+ ({nameof(medium.Name)}, MediumType)
+ VALUES
+ (@{nameof(medium.Name)}, @{nameof(medium.MediaType)});
+ SELECT last_insert_rowid()", medium);
+
+ medium.Id = (int)newIds.First();
+ }
+
+ private async Task> GetAllMediumsAsync(SqliteConnection db)
+ {
+ //// IMPLEMENTATION WITHOUT DAPPER
+ //IList mediums = new List();
+ //using var selectCommand = new SqliteCommand("SELECT Id, Name, MediumType FROM Mediums", db);
+ //using SqliteDataReader query = await selectCommand.ExecuteReaderAsync();
+
+ //while (query.Read())
+ //{
+ // var medium = new Medium
+ // {
+ // Id = query.GetInt32(0),
+ // Name = query.GetString(1),
+ // MediaType = (ItemType)query.GetInt32(2)
+ // };
+ // mediums.Add(medium);
+ //}
+
+ //return mediums;
+
+ var mediums =
+ await db.QueryAsync(@"SELECT Id,
+ Name,
+ MediumType AS MediaType
+ FROM Mediums");
+
+ return mediums.ToList();
+ }
+
+ private async Task> GetAllMediaItemsAsync(SqliteConnection db)
+ {
+ var itemsResult = await db.QueryAsync
+ (
+ @"SELECT
+ [MediaItems].[Id],
+ [MediaItems].[Name],
+ [MediaItems].[ItemType] AS MediaType,
+ [MediaItems].[LocationType] AS Location,
+ [Mediums].[Id],
+ [Mediums].[Name],
+ [Mediums].[MediumType] AS MediaType
+ FROM
+ [MediaItems]
+ JOIN
+ [Mediums]
+ ON
+ [Mediums].[Id] = [MediaItems].[MediumId]",
+ (item, medium) =>
+ {
+ item.MediumInfo = medium;
+ return item;
+ }
+ );
+
+ return itemsResult.ToList();
+ }
+
+ private async Task InsertMediaItemAsync(SqliteConnection db, MediaItem item)
+ {
+ var newIds = await db.QueryAsync(
+ @"INSERT INTO MediaItems
+ (Name, ItemType, MediumId, LocationType)
+ VALUES
+ (@Name, @MediaType, @MediumId, @Location);
+ SELECT last_insert_rowid()", item);
+
+ return (int)newIds.First();
+ }
+
+ private async Task UpdateMediaItemAsync(SqliteConnection db, MediaItem item)
+ {
+ await db.QueryAsync(
+ @"UPDATE MediaItems
+ SET Name = @Name,
+ ItemType = @MediaType,
+ MediumId = @MediumId,
+ LocationType = @Location
+ WHERE Id = @Id;", item);
+ }
+
+ private async Task DeleteMediaItemAsync(SqliteConnection db, int id)
+ {
+ await db.DeleteAsync(new MediaItem { Id = id });
+ }
+
+ private async Task PopulateMediumsAsync(SqliteConnection db)
+ {
+ _mediums = await GetAllMediumsAsync(db);
+
+ if (_mediums.Count == 0)
+ {
+ var cd = new Medium { Id = 1, MediaType = ItemType.Music, Name = "CD" };
+ var vinyl = new Medium { Id = 2, MediaType = ItemType.Music, Name = "Vinyl" };
+ var hardcover = new Medium { Id = 3, MediaType = ItemType.Book, Name = "Hardcover" };
+ var paperback = new Medium { Id = 4, MediaType = ItemType.Book, Name = "Paperback" };
+ var dvd = new Medium { Id = 5, MediaType = ItemType.Video, Name = "DVD" };
+ var bluRay = new Medium { Id = 6, MediaType = ItemType.Video, Name = "Blu Ray" };
+
+ var mediums = new List
+ {
+ cd,
+ vinyl,
+ hardcover,
+ paperback,
+ dvd,
+ bluRay
+ };
+
+ foreach (var medium in mediums)
+ {
+ await InsertMediumAsync(db, medium);
+ }
+
+ _mediums = await GetAllMediumsAsync(db);
+ }
+ }
+
+ private void PopulateItemTypes()
+ {
+ _itemTypes = new List
+ {
+ ItemType.Book,
+ ItemType.Music,
+ ItemType.Video
+ };
+ }
+
+ private void PopulateLocationTypes()
+ {
+ _locationTypes = new List
+ {
+ LocationType.InCollection,
+ LocationType.Loaned
+ };
+ }
+
+ public async Task InitializeDataAsync()
+ {
+ using (var db = await GetOpenConnectionAsync())
+ {
+ await CreateMediumTableAsync(db);
+ await CreateMediaItemTableAsync(db);
+
+ PopulateItemTypes();
+ await PopulateMediumsAsync(db);
+ PopulateLocationTypes();
+ }
+ }
+
+ public async Task AddItemAsync(MediaItem item)
+ {
+ using var db = await GetOpenConnectionAsync();
+ return await InsertMediaItemAsync(db, item);
+ }
+
+ public async Task GetItemAsync(int id)
+ {
+ IList mediaItems;
+ using var db = await GetOpenConnectionAsync();
+ mediaItems = await GetAllMediaItemsAsync(db);
+
+ //Filter the list to get the item for our Id
+ return mediaItems.FirstOrDefault(i => i.Id == id);
+ }
+
+ public async Task> GetItemsAsync()
+ {
+ using var db = await GetOpenConnectionAsync();
+ return await GetAllMediaItemsAsync(db);
+ }
+
+ public IList GetItemTypes()
+ {
+ return _itemTypes;
+ }
+
+ public IList GetLocationTypes()
+ {
+ return _locationTypes;
+ }
+
+ public Medium GetMedium(string name)
+ {
+ return _mediums.FirstOrDefault(m => m.Name == name);
+ }
+
+ public Medium GetMedium(int id)
+ {
+ return _mediums.FirstOrDefault(m => m.Id == id);
+ }
+
+ public IList GetMediums()
+ {
+ return _mediums;
+ }
+
+ public IList GetMediums(ItemType itemType)
+ {
+ return _mediums
+ .Where(m => m.MediaType == itemType)
+ .ToList();
+ }
+
+ public async Task UpdateItemAsync(MediaItem item)
+ {
+ using var db = await GetOpenConnectionAsync();
+ await UpdateMediaItemAsync(db, item);
+ }
+
+ public async Task DeleteItemAsync(MediaItem item)
+ {
+ using var db = await GetOpenConnectionAsync();
+ await DeleteMediaItemAsync(db, item.Id);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/ViewModels/ItemDetailsViewModel.cs b/Chapter11/MyMediaCollection/MyMediaCollection/ViewModels/ItemDetailsViewModel.cs
new file mode 100644
index 0000000..1054800
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/ViewModels/ItemDetailsViewModel.cs
@@ -0,0 +1,142 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using MyMediaCollection.Enums;
+using MyMediaCollection.Interfaces;
+using MyMediaCollection.Model;
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace MyMediaCollection.ViewModels
+{
+ public partial class ItemDetailsViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ private ObservableCollection locationTypes = new();
+ [ObservableProperty]
+ private ObservableCollection mediums = new();
+ [ObservableProperty]
+ private ObservableCollection itemTypes = new();
+ private int _itemId;
+ [ObservableProperty]
+ private string itemName;
+ [ObservableProperty]
+ private string selectedMedium;
+ [ObservableProperty]
+ private string selectedItemType;
+ [ObservableProperty]
+ private string selectedLocation;
+ [ObservableProperty]
+ private bool isDirty;
+ private int _selectedItemId = -1;
+ protected INavigationService _navigationService;
+ protected IDataService _dataService;
+
+ public ItemDetailsViewModel(INavigationService navigationService, IDataService dataService)
+ {
+ _navigationService = navigationService;
+ _dataService = dataService;
+
+ PopulateLists();
+ }
+
+ public void InitializeItemDetailData(int itemId)
+ {
+ _selectedItemId = itemId;
+ IsDirty = false;
+ }
+
+ private void PopulateLists()
+ {
+ ItemTypes.Clear();
+ foreach (string iType in Enum.GetNames(typeof(ItemType)))
+ ItemTypes.Add(iType);
+
+ LocationTypes.Clear();
+ foreach (string lType in Enum.GetNames(typeof(LocationType)))
+ LocationTypes.Add(lType);
+
+ Mediums = new ObservableCollection();
+ }
+
+ private async Task SaveAsync()
+ {
+ MediaItem item;
+
+ if (_itemId > 0)
+ {
+ item = await _dataService.GetItemAsync(_itemId);
+
+ item.Name = ItemName;
+ item.Location = (LocationType)Enum.Parse(typeof(LocationType), SelectedLocation);
+ item.MediaType = (ItemType)Enum.Parse(typeof(ItemType), SelectedItemType);
+ item.MediumInfo = _dataService.GetMedium(SelectedMedium);
+
+ await _dataService.UpdateItemAsync(item);
+ }
+ else
+ {
+ item = new MediaItem
+ {
+ Name = ItemName,
+ Location = (LocationType)Enum.Parse(typeof(LocationType), SelectedLocation),
+ MediaType = (ItemType)Enum.Parse(typeof(ItemType), SelectedItemType),
+ MediumInfo = _dataService.GetMedium(SelectedMedium)
+ };
+
+ await _dataService.AddItemAsync(item);
+ }
+ }
+
+ public async Task SaveItemAndContinueAsync()
+ {
+ await SaveAsync();
+ _itemId = 0;
+ ItemName = string.Empty;
+ SelectedMedium = null;
+ SelectedLocation = null;
+ SelectedItemType = null;
+ IsDirty = false;
+ }
+
+ public async Task SaveItemAndReturnAsync()
+ {
+ await SaveAsync();
+ _navigationService.GoBack();
+ }
+
+ partial void OnItemNameChanged(string value)
+ {
+ IsDirty = true;
+ }
+
+ partial void OnSelectedMediumChanged(string value)
+ {
+ IsDirty = true;
+ }
+
+ partial void OnSelectedItemTypeChanged(string value)
+ {
+ IsDirty = true;
+ Mediums.Clear();
+
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ foreach (string med in _dataService.GetMediums((ItemType)Enum.Parse(typeof(ItemType), SelectedItemType)).Select(m => m.Name))
+ Mediums.Add(med);
+ }
+ }
+
+ partial void OnSelectedLocationChanged(string value)
+ {
+ IsDirty = true;
+ }
+
+ [RelayCommand]
+ private void Cancel()
+ {
+ _navigationService.GoBack();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/ViewModels/MainViewModel.cs b/Chapter11/MyMediaCollection/MyMediaCollection/ViewModels/MainViewModel.cs
new file mode 100644
index 0000000..d718f18
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/ViewModels/MainViewModel.cs
@@ -0,0 +1,121 @@
+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;
+using System.Threading.Tasks;
+
+namespace MyMediaCollection.ViewModels
+{
+ public partial class MainViewModel : ObservableObject
+ {
+ [ObservableProperty]
+ private string selectedMedium;
+ [ObservableProperty]
+ private ObservableCollection items = new ObservableCollection();
+ private ObservableCollection allItems;
+ [ObservableProperty]
+ private ObservableCollection mediums;
+ [ObservableProperty]
+ [NotifyCanExecuteChangedFor(nameof(DeleteCommand))]
+ private MediaItem selectedMediaItem;
+ private INavigationService _navigationService;
+ private IDataService _dataService;
+ private const string AllMediums = "All";
+
+ public MainViewModel(INavigationService navigationService, IDataService dataService)
+ {
+ _navigationService = navigationService;
+ _dataService = dataService;
+
+ PopulateDataAsync();
+ }
+
+ public async Task PopulateDataAsync()
+ {
+ Items.Clear();
+
+ foreach (var item in await _dataService.GetItemsAsync())
+ {
+ Items.Add(item);
+ }
+
+ allItems = new ObservableCollection(Items);
+
+ Mediums = new ObservableCollection
+ {
+ AllMediums
+ };
+
+ foreach (var itemType in _dataService.GetItemTypes())
+ {
+ Mediums.Add(itemType.ToString());
+ }
+
+ SelectedMedium = Mediums[0];
+ }
+
+ partial void OnSelectedMediumChanged(string value)
+ {
+ Items.Clear();
+
+ foreach (var item in allItems)
+ {
+ if (string.IsNullOrWhiteSpace(value)
+ || value == "All"
+ || value == item.MediaType.ToString())
+ {
+ Items.Add(item);
+ }
+ }
+ }
+
+ [RelayCommand]
+ private void AddEdit()
+ {
+ var selectedItemId = -1;
+
+ if (SelectedMediaItem != null)
+ {
+ selectedItemId = SelectedMediaItem.Id;
+ }
+
+ _navigationService.NavigateTo("ItemDetailsPage", selectedItemId);
+ }
+
+ public void ListViewDoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
+ {
+ AddEdit();
+ }
+
+ [RelayCommand(CanExecute = nameof(CanDeleteItem))]
+ private async Task DeleteAsync()
+ {
+ await _dataService.DeleteItemAsync(SelectedMediaItem);
+ allItems.Remove(SelectedMediaItem);
+ Items.Remove(SelectedMediaItem);
+ }
+
+ 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/Chapter11/MyMediaCollection/MyMediaCollection/Views/ItemDetailsPage.xaml b/Chapter11/MyMediaCollection/MyMediaCollection/Views/ItemDetailsPage.xaml
new file mode 100644
index 0000000..1431228
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Views/ItemDetailsPage.xaml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Views/ItemDetailsPage.xaml.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Views/ItemDetailsPage.xaml.cs
new file mode 100644
index 0000000..524dbd2
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Views/ItemDetailsPage.xaml.cs
@@ -0,0 +1,60 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Navigation;
+using MyMediaCollection.ViewModels;
+
+namespace MyMediaCollection.Views
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class ItemDetailsPage : Page
+ {
+ public ItemDetailsPage()
+ {
+ ViewModel = App.HostContainer.Services.GetService();
+
+ this.InitializeComponent();
+ Loaded += ItemDetailsPage_Loaded;
+
+ Windows.Storage.ApplicationDataContainer localSettings = Windows.Storage.ApplicationData.Current.LocalSettings;
+
+ // Load the user setting
+ string haveExplainedSaveSetting = localSettings.Values[nameof(SavingTip)] as string;
+
+ // If the user has not seen the save tip, display it
+
+ if (!bool.TryParse(haveExplainedSaveSetting, out bool result) || !result)
+ {
+ SavingTip.IsOpen = true;
+
+ // Save the teaching tip setting
+ localSettings.Values[nameof(SavingTip)] = "true";
+ }
+ }
+
+ private void ItemDetailsPage_Loaded(object sender, RoutedEventArgs e)
+ {
+ var mainWindow = (Application.Current as App)?.Window as MainWindow;
+ if (mainWindow != null)
+ {
+ mainWindow.SetPageTitle("Item Details");
+ }
+ }
+
+ public ItemDetailsViewModel ViewModel;
+
+ protected override void OnNavigatedTo(NavigationEventArgs e)
+ {
+ base.OnNavigatedTo(e);
+
+ var itemId = (int)e.Parameter;
+
+ if (itemId > 0)
+ {
+ ViewModel.InitializeItemDetailData(itemId);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Views/MainPage.xaml b/Chapter11/MyMediaCollection/MyMediaCollection/Views/MainPage.xaml
new file mode 100644
index 0000000..b5ed187
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Views/MainPage.xaml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/Views/MainPage.xaml.cs b/Chapter11/MyMediaCollection/MyMediaCollection/Views/MainPage.xaml.cs
new file mode 100644
index 0000000..b1786e8
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/Views/MainPage.xaml.cs
@@ -0,0 +1,93 @@
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using MyMediaCollection.ViewModels;
+using MyMediaCollection.Helpers;
+
+namespace MyMediaCollection.Views
+{
+ ///
+ /// An empty page that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class MainPage : Page
+ {
+ public static MainPage Current;
+
+ public MainPage()
+ {
+ ViewModel = App.HostContainer.Services.GetService();
+ this.InitializeComponent();
+ Current = this;
+ Loaded += MainPage_Loaded;
+ }
+
+ private void MainPage_Loaded(object sender, RoutedEventArgs e)
+ {
+ var mainWindow = (Application.Current as App)?.Window as MainWindow;
+ if (mainWindow != null)
+ {
+ mainWindow.SetPageTitle("Home");
+ }
+ }
+
+ 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
diff --git a/Chapter11/MyMediaCollection/MyMediaCollection/app.manifest b/Chapter11/MyMediaCollection/MyMediaCollection/app.manifest
new file mode 100644
index 0000000..abc9f68
--- /dev/null
+++ b/Chapter11/MyMediaCollection/MyMediaCollection/app.manifest
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true/PM
+ PerMonitorV2, PerMonitor
+
+
+
\ No newline at end of file
diff --git a/Chapter11/XamlDebugging/App.xaml b/Chapter11/XamlDebugging/App.xaml
new file mode 100644
index 0000000..0e628b8
--- /dev/null
+++ b/Chapter11/XamlDebugging/App.xaml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter11/XamlDebugging/App.xaml.cs b/Chapter11/XamlDebugging/App.xaml.cs
new file mode 100644
index 0000000..6ad7250
--- /dev/null
+++ b/Chapter11/XamlDebugging/App.xaml.cs
@@ -0,0 +1,50 @@
+using Microsoft.UI.Xaml;
+using Microsoft.UI.Xaml.Controls;
+using Microsoft.UI.Xaml.Controls.Primitives;
+using Microsoft.UI.Xaml.Data;
+using Microsoft.UI.Xaml.Input;
+using Microsoft.UI.Xaml.Media;
+using Microsoft.UI.Xaml.Navigation;
+using Microsoft.UI.Xaml.Shapes;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Runtime.InteropServices.WindowsRuntime;
+using Windows.ApplicationModel;
+using Windows.ApplicationModel.Activation;
+using Windows.Foundation;
+using Windows.Foundation.Collections;
+
+// To learn more about WinUI, the WinUI project structure,
+// and more about our project templates, see: http://aka.ms/winui-project-info.
+
+namespace XamlDebugging
+{
+ ///
+ /// Provides application-specific behavior to supplement the default Application class.
+ ///
+ public partial class App : Application
+ {
+ ///
+ /// Initializes the singleton application object. This is the first line of authored code
+ /// executed, and as such is the logical equivalent of main() or WinMain().
+ ///
+ public App()
+ {
+ this.InitializeComponent();
+ }
+
+ ///
+ /// Invoked when the application is launched.
+ ///
+ /// Details about the launch request and process.
+ protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
+ {
+ m_window = new MainWindow();
+ m_window.Activate();
+ }
+
+ private Window m_window;
+ }
+}
diff --git a/Chapter11/XamlDebugging/Assets/LockScreenLogo.scale-200.png b/Chapter11/XamlDebugging/Assets/LockScreenLogo.scale-200.png
new file mode 100644
index 0000000..7440f0d
Binary files /dev/null and b/Chapter11/XamlDebugging/Assets/LockScreenLogo.scale-200.png differ
diff --git a/Chapter11/XamlDebugging/Assets/SplashScreen.scale-200.png b/Chapter11/XamlDebugging/Assets/SplashScreen.scale-200.png
new file mode 100644
index 0000000..32f486a
Binary files /dev/null and b/Chapter11/XamlDebugging/Assets/SplashScreen.scale-200.png differ
diff --git a/Chapter11/XamlDebugging/Assets/Square150x150Logo.scale-200.png b/Chapter11/XamlDebugging/Assets/Square150x150Logo.scale-200.png
new file mode 100644
index 0000000..53ee377
Binary files /dev/null and b/Chapter11/XamlDebugging/Assets/Square150x150Logo.scale-200.png differ
diff --git a/Chapter11/XamlDebugging/Assets/Square44x44Logo.scale-200.png b/Chapter11/XamlDebugging/Assets/Square44x44Logo.scale-200.png
new file mode 100644
index 0000000..f713bba
Binary files /dev/null and b/Chapter11/XamlDebugging/Assets/Square44x44Logo.scale-200.png differ
diff --git a/Chapter11/XamlDebugging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/Chapter11/XamlDebugging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png
new file mode 100644
index 0000000..dc9f5be
Binary files /dev/null and b/Chapter11/XamlDebugging/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ
diff --git a/Chapter11/XamlDebugging/Assets/StoreLogo.png b/Chapter11/XamlDebugging/Assets/StoreLogo.png
new file mode 100644
index 0000000..a4586f2
Binary files /dev/null and b/Chapter11/XamlDebugging/Assets/StoreLogo.png differ
diff --git a/Chapter11/XamlDebugging/Assets/Wide310x150Logo.scale-200.png b/Chapter11/XamlDebugging/Assets/Wide310x150Logo.scale-200.png
new file mode 100644
index 0000000..8b4a5d0
Binary files /dev/null and b/Chapter11/XamlDebugging/Assets/Wide310x150Logo.scale-200.png differ
diff --git a/Chapter11/XamlDebugging/MainWindow.xaml b/Chapter11/XamlDebugging/MainWindow.xaml
new file mode 100644
index 0000000..2440744
--- /dev/null
+++ b/Chapter11/XamlDebugging/MainWindow.xaml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Chapter11/XamlDebugging/MainWindow.xaml.cs b/Chapter11/XamlDebugging/MainWindow.xaml.cs
new file mode 100644
index 0000000..d057526
--- /dev/null
+++ b/Chapter11/XamlDebugging/MainWindow.xaml.cs
@@ -0,0 +1,15 @@
+using Microsoft.UI.Xaml;
+
+namespace XamlDebugging
+{
+ ///
+ /// An empty window that can be used on its own or navigated to within a Frame.
+ ///
+ public sealed partial class MainWindow : Window
+ {
+ public MainWindow()
+ {
+ this.InitializeComponent();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/XamlDebugging/Package.appxmanifest b/Chapter11/XamlDebugging/Package.appxmanifest
new file mode 100644
index 0000000..667c802
--- /dev/null
+++ b/Chapter11/XamlDebugging/Package.appxmanifest
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+ XamlDebugging
+ alash
+ Assets\StoreLogo.png
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Chapter11/XamlDebugging/Properties/launchSettings.json b/Chapter11/XamlDebugging/Properties/launchSettings.json
new file mode 100644
index 0000000..5a72567
--- /dev/null
+++ b/Chapter11/XamlDebugging/Properties/launchSettings.json
@@ -0,0 +1,10 @@
+{
+ "profiles": {
+ "XamlDebugging (Package)": {
+ "commandName": "MsixPackage"
+ },
+ "XamlDebugging (Unpackaged)": {
+ "commandName": "Project"
+ }
+ }
+}
\ No newline at end of file
diff --git a/Chapter11/XamlDebugging/XamlDebugging.csproj b/Chapter11/XamlDebugging/XamlDebugging.csproj
new file mode 100644
index 0000000..8b49ce2
--- /dev/null
+++ b/Chapter11/XamlDebugging/XamlDebugging.csproj
@@ -0,0 +1,48 @@
+
+
+ WinExe
+ net6.0-windows10.0.19041.0
+ 10.0.17763.0
+ XamlDebugging
+ app.manifest
+ x86;x64;ARM64
+ win10-x86;win10-x64;win10-arm64
+ win10-$(Platform).pubxml
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
diff --git a/Chapter11/XamlDebugging/XamlDebugging.sln b/Chapter11/XamlDebugging/XamlDebugging.sln
new file mode 100644
index 0000000..7eb0a90
--- /dev/null
+++ b/Chapter11/XamlDebugging/XamlDebugging.sln
@@ -0,0 +1,43 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.7.34009.444
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "XamlDebugging", "XamlDebugging.csproj", "{6682E0D3-C936-42E9-915A-B497C4A7BE9A}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|ARM64 = Debug|ARM64
+ Debug|x64 = Debug|x64
+ Debug|x86 = Debug|x86
+ Release|ARM64 = Release|ARM64
+ Release|x64 = Release|x64
+ Release|x86 = Release|x86
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|ARM64.Build.0 = Debug|ARM64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|ARM64.Deploy.0 = Debug|ARM64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|x64.ActiveCfg = Debug|x64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|x64.Build.0 = Debug|x64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|x64.Deploy.0 = Debug|x64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|x86.ActiveCfg = Debug|x86
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|x86.Build.0 = Debug|x86
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Debug|x86.Deploy.0 = Debug|x86
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|ARM64.ActiveCfg = Release|ARM64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|ARM64.Build.0 = Release|ARM64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|ARM64.Deploy.0 = Release|ARM64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|x64.ActiveCfg = Release|x64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|x64.Build.0 = Release|x64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|x64.Deploy.0 = Release|x64
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|x86.ActiveCfg = Release|x86
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|x86.Build.0 = Release|x86
+ {6682E0D3-C936-42E9-915A-B497C4A7BE9A}.Release|x86.Deploy.0 = Release|x86
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {B852D63C-F858-4739-A7D3-AADC3202D619}
+ EndGlobalSection
+EndGlobal
diff --git a/Chapter11/XamlDebugging/app.manifest b/Chapter11/XamlDebugging/app.manifest
new file mode 100644
index 0000000..40900fd
--- /dev/null
+++ b/Chapter11/XamlDebugging/app.manifest
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PerMonitorV2
+
+
+
\ No newline at end of file