diff --git a/src/SampleApp/App.xaml b/src/SampleApp/App.xaml index 010590c..88e6948 100644 --- a/src/SampleApp/App.xaml +++ b/src/SampleApp/App.xaml @@ -1,16 +1,23 @@ - - - - - - - - - - - - + + + + + + + + + + + #320C171E + #321D3849 + #3224465B + #3233637F + + + + diff --git a/src/SampleApp/App.xaml.cs b/src/SampleApp/App.xaml.cs index 9c40589..b9c6f6d 100644 --- a/src/SampleApp/App.xaml.cs +++ b/src/SampleApp/App.xaml.cs @@ -1,50 +1,540 @@ -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 SampleApp -{ - /// - /// 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; - } -} +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Media.Imaging; + +using Windows.UI.Popups; +using Windows.UI.ViewManagement; + +namespace SampleApp; + +/// +/// I've modified the UX/UI and added the "SingleClickEdit" feature. +/// Forked from https://github.com/w-ahmad/WinUI.TableView +/// +public partial class App : Application +{ + int m_width = 1300; + int m_height = 700; + Window m_window = null; + static UISettings m_UISettings = new UISettings(); + public static bool IsClosing { get; set; } = false; + public static IntPtr WindowHandle { get; set; } + public static FrameworkElement? MainRoot { get; set; } + + // We won't configure backing fields for these as the user could adjust them during app lifetime. + public static bool TransparencyEffectsEnabled + { + get => m_UISettings.AdvancedEffectsEnabled; + } + public static bool AnimationsEffectsEnabled + { + get => m_UISettings.AnimationsEnabled; + } + public static double TextScaleFactor + { + get => m_UISettings.TextScaleFactor; + } + public static bool AutoHideScrollbars + { + get => m_UISettings.AutoHideScrollBars; + } + + public static string TimeStamp + { + get => DateTime.Now.ToString("yyyy-MM-dd hh:mm:ss.fff tt"); + } + + /// + /// 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() + { + Debug.WriteLine($"[INFO] {System.Reflection.MethodBase.GetCurrentMethod()?.DeclaringType?.Name}__{System.Reflection.MethodBase.GetCurrentMethod()?.Name} [{TimeStamp}]"); + + App.Current.DebugSettings.FailFastOnErrors = false; + + #region [Exception handlers] + AppDomain.CurrentDomain.UnhandledException += CurrentDomainUnhandledException; + AppDomain.CurrentDomain.FirstChanceException += CurrentDomainFirstChanceException; + AppDomain.CurrentDomain.ProcessExit += CurrentDomainOnProcessExit; + TaskScheduler.UnobservedTaskException += OnUnobservedTaskException; + UnhandledException += ApplicationUnhandledException; + #endregion + + this.InitializeComponent(); + + foreach (var ra in GetReferencedAssemblies()) + { + Debug.WriteLine($"[INFO] {ra.Key} {ra.Value}"); + } + } + + /// + /// 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(); + + var appWin = GetAppWindow(m_window); + if (appWin != null) + { + // We don't have the Closing event exposed by default, so we'll use the AppWindow to compensate. + appWin.Closing += (s, e) => + { + App.IsClosing = true; + Debug.WriteLine($"[INFO] Application closing detected at {TimeStamp}"); + }; + + // Destroying is always called, but Closing is only called when the application is shutdown normally. + appWin.Destroying += (s, e) => + { + Debug.WriteLine($"[INFO] Application destroying detected at {TimeStamp}"); + }; + + // The changed event holds a bunch of juicy info that we can extrapolate. + appWin.Changed += (s, args) => + { + if (args.DidSizeChange) + { + Debug.WriteLine($"[INFO] Window size is now {s.Size.Width},{s.Size.Height}"); + } + if (args.DidPositionChange) + { + if (s.Presenter is Microsoft.UI.Windowing.OverlappedPresenter op && op.State != Microsoft.UI.Windowing.OverlappedPresenterState.Maximized) + Debug.WriteLine($"[INFO] Window position is now {s.Position.X},{s.Position.Y}"); + } + }; + appWin.SetIcon(System.IO.Path.Combine(AppContext.BaseDirectory, $"Assets/StoreLogoAlt.ico")); + appWin.TitleBar.IconShowOptions = Microsoft.UI.Windowing.IconShowOptions.ShowIconAndSystemMenu; + appWin.Resize(new Windows.Graphics.SizeInt32(m_width, m_height)); + } + + m_window.Activate(); + MainRoot = m_window.Content as FrameworkElement; // Save the FrameworkElement for any future content dialogs. + CenterWindow(m_window); + } + + #region [Window Helpers] + /// + /// This code example demonstrates how to retrieve an AppWindow from a WinUI3 window. + /// The AppWindow class is available for any top-level HWND in your app. + /// AppWindow is available only to desktop apps (both packaged and unpackaged), it's not available to UWP apps. + /// https://learn.microsoft.com/en-us/windows/apps/windows-app-sdk/windowing/windowing-overview + /// https://learn.microsoft.com/en-us/windows/windows-app-sdk/api/winrt/microsoft.ui.windowing.appwindow.create?view=windows-app-sdk-1.3 + /// + public Microsoft.UI.Windowing.AppWindow? GetAppWindow(object window) + { + // Retrieve the window handle (HWND) of the current (XAML) WinUI3 window. + var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window); + + // For other classes to use (mostly P/Invoke). + App.WindowHandle = hWnd; + + // Retrieve the WindowId that corresponds to hWnd. + Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd); + + // Lastly, retrieve the AppWindow for the current (XAML) WinUI3 window. + Microsoft.UI.Windowing.AppWindow appWindow = Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId); + + return appWindow; + } + + /// + /// Centers a based on the . + /// + /// This must be run on the UI thread. + public static void CenterWindow(Window window) + { + if (window == null) { return; } + + try + { + System.IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window); + Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd); + if (Microsoft.UI.Windowing.AppWindow.GetFromWindowId(windowId) is Microsoft.UI.Windowing.AppWindow appWindow && + Microsoft.UI.Windowing.DisplayArea.GetFromWindowId(windowId, Microsoft.UI.Windowing.DisplayAreaFallback.Nearest) is Microsoft.UI.Windowing.DisplayArea displayArea) + { + Windows.Graphics.PointInt32 CenteredPosition = appWindow.Position; + CenteredPosition.X = (displayArea.WorkArea.Width - appWindow.Size.Width) / 2; + CenteredPosition.Y = (displayArea.WorkArea.Height - appWindow.Size.Height) / 2; + appWindow.Move(CenteredPosition); + } + } + catch (Exception ex) + { + Debug.WriteLine($"[ERROR] {MethodBase.GetCurrentMethod()?.Name}: {ex.Message}"); + } + } + + /// + /// The exposes properties such as: + /// OuterBounds (Rect32) + /// WorkArea.Width (int) + /// WorkArea.Height (int) + /// IsPrimary (bool) + /// DisplayId.Value (ulong) + /// + /// + /// + public Microsoft.UI.Windowing.DisplayArea? GetDisplayArea(Window window) + { + try + { + System.IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window); + Microsoft.UI.WindowId windowId = Microsoft.UI.Win32Interop.GetWindowIdFromWindow(hWnd); + var da = Microsoft.UI.Windowing.DisplayArea.GetFromWindowId(windowId, Microsoft.UI.Windowing.DisplayAreaFallback.Nearest); + return da; + } + catch (Exception ex) + { + Debug.WriteLine($"[ERROR] {MethodBase.GetCurrentMethod()?.Name}: {ex.Message}"); + return null; + } + } + #endregion + + #region [Dialog Helpers] + static SemaphoreSlim semaSlim = new SemaphoreSlim(1, 1); + /// + /// The does not look as nice as the + /// and is not part of the native Microsoft.UI.Xaml.Controls. + /// The offers the + /// callback, but this could be replaced with actions. Both can be shown asynchronously. + /// + /// + /// You'll need to call when using the , + /// because the does not exist and an owner must be defined. + /// + public static async Task ShowMessageBox(string title, string message, string yesText, string noText, Action? yesAction, Action? noAction) + { + if (App.WindowHandle == IntPtr.Zero) { return; } + + // Create the dialog. + var messageDialog = new MessageDialog($"{message}"); + messageDialog.Title = title; + + if (!string.IsNullOrEmpty(yesText)) + { + messageDialog.Commands.Add(new UICommand($"{yesText}", (opt) => { yesAction?.Invoke(); })); + messageDialog.DefaultCommandIndex = 0; + } + + if (!string.IsNullOrEmpty(noText)) + { + messageDialog.Commands.Add(new UICommand($"{noText}", (opt) => { noAction?.Invoke(); })); + messageDialog.DefaultCommandIndex = 1; + } + + // We must initialize the dialog with an owner. + WinRT.Interop.InitializeWithWindow.Initialize(messageDialog, App.WindowHandle); + // Show the message dialog. Our DialogDismissedHandler will deal with what selection the user wants. + await messageDialog.ShowAsync(); + // We could force the result in a separate timer... + //DialogDismissedHandler(new UICommand("time-out")); + } + + /// + /// The does not look as nice as the + /// and is not part of the native Microsoft.UI.Xaml.Controls. + /// The offers the + /// callback, but this could be replaced with actions. Both can be shown asynchronously. + /// + /// + /// You'll need to call when using the , + /// because the does not exist and an owner must be defined. + /// + public static async Task ShowMessageBox(string title, string message, string primaryText, string cancelText) + { + // Create the dialog. + var messageDialog = new MessageDialog($"{message}"); + messageDialog.Title = title; + + if (!string.IsNullOrEmpty(primaryText)) + { + messageDialog.Commands.Add(new UICommand($"{primaryText}", new UICommandInvokedHandler(DialogDismissedHandler))); + messageDialog.DefaultCommandIndex = 0; + } + + if (!string.IsNullOrEmpty(cancelText)) + { + messageDialog.Commands.Add(new UICommand($"{cancelText}", new UICommandInvokedHandler(DialogDismissedHandler))); + messageDialog.DefaultCommandIndex = 1; + } + // We must initialize the dialog with an owner. + WinRT.Interop.InitializeWithWindow.Initialize(messageDialog, App.WindowHandle); + // Show the message dialog. Our DialogDismissedHandler will deal with what selection the user wants. + await messageDialog.ShowAsync(); + + // We could force the result in a separate timer... + //DialogDismissedHandler(new UICommand("time-out")); + } + + /// + /// Callback for the selected option from the user. + /// + static void DialogDismissedHandler(IUICommand command) + { + Debug.WriteLine($"[INFO] UICommand.Label ⇨ {command.Label}"); + } + + /// + /// The looks much better than the + /// and is part of the native Microsoft.UI.Xaml.Controls. + /// The does not offer a + /// callback, but in this example was replaced with actions. Both can be shown asynchronously. + /// + /// + /// There is no need to call when using the , + /// but a must be defined since it inherits from . + /// The was added to prevent "COMException: Only one ContentDialog can be opened at a time." + /// + public static async Task ShowDialogBox(string title, string message, string primaryText, string cancelText, Action? onPrimary, Action? onCancel, Uri? imageUri) + { + if (App.MainRoot?.XamlRoot == null) { return; } + + await semaSlim.WaitAsync(); + + #region [Initialize Assets] + double fontSize = 16; + Microsoft.UI.Xaml.Media.FontFamily fontFamily = new Microsoft.UI.Xaml.Media.FontFamily("Consolas"); + + if (App.Current.Resources.TryGetValue("FontSizeMedium", out object _)) + fontSize = (double)App.Current.Resources["FontSizeMedium"]; + + if (App.Current.Resources.TryGetValue("PrimaryFont", out object _)) + fontFamily = (Microsoft.UI.Xaml.Media.FontFamily)App.Current.Resources["PrimaryFont"]; + + StackPanel panel = new StackPanel() + { + Orientation = Microsoft.UI.Xaml.Controls.Orientation.Vertical, + Spacing = 10d + }; + + if (imageUri is not null) + { + panel.Children.Add(new Image + { + Margin = new Thickness(1, -50, 1, 1), // Move the image into the title area. + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Right, + Stretch = Microsoft.UI.Xaml.Media.Stretch.UniformToFill, + Width = 40, + Height = 40, + Source = new BitmapImage(imageUri) + }); + } + + panel.Children.Add(new TextBlock() + { + Text = message, + FontSize = fontSize, + FontFamily = fontFamily, + HorizontalAlignment = Microsoft.UI.Xaml.HorizontalAlignment.Left, + TextWrapping = Microsoft.UI.Xaml.TextWrapping.Wrap + }); + + var tb = new TextBox() + { + Text = message, + FontSize = fontSize, + FontFamily = fontFamily, + TextWrapping = TextWrapping.Wrap + }; + tb.Loaded += (s, e) => { tb.SelectAll(); }; + #endregion + + // NOTE: Content dialogs will automatically darken the background. + ContentDialog contentDialog = new ContentDialog() + { + Title = title, + PrimaryButtonText = primaryText, + CloseButtonText = cancelText, + Content = panel, + XamlRoot = App.MainRoot?.XamlRoot, + RequestedTheme = App.MainRoot?.ActualTheme ?? ElementTheme.Default + }; + + try + { + ContentDialogResult result = await contentDialog.ShowAsync(); + + switch (result) + { + case ContentDialogResult.Primary: + onPrimary?.Invoke(); + break; + //case ContentDialogResult.Secondary: + // onSecondary?.Invoke(); + // break; + case ContentDialogResult.None: // Cancel + onCancel?.Invoke(); + break; + default: + Debug.WriteLine($"Dialog result not defined."); + break; + } + } + catch (System.Runtime.InteropServices.COMException ex) + { + Debug.WriteLine($"[ERROR] ShowDialogBox: {ex.Message}"); + } + finally + { + semaSlim.Release(); + } + } + #endregion + + #region [Domain Events] + void ApplicationUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e) + { + // https://docs.microsoft.com/windows/windows-app-sdk/api/winrt/microsoft.ui.xaml.application.unhandledexception. + Exception? ex = e.Exception; + Debug.WriteLine($"[UnhandledException]: {ex?.Message}"); + Debug.WriteLine($"⇨ Unhandled exception of type {ex?.GetType()}: {ex}"); + DebugLog($"Unhandled Exception StackTrace: {Environment.StackTrace}"); + e.Handled = true; + } + + void CurrentDomainOnProcessExit(object? sender, EventArgs e) + { + if (!IsClosing) + IsClosing = true; + + if (sender is null) + return; + + if (sender is AppDomain ad) + { + Debug.WriteLine($"[OnProcessExit]", $"{nameof(App)}"); + Debug.WriteLine($"⇨ DomainID: {ad.Id}", $"{nameof(App)}"); + Debug.WriteLine($"⇨ FriendlyName: {ad.FriendlyName}", $"{nameof(App)}"); + Debug.WriteLine($"⇨ BaseDirectory: {ad.BaseDirectory}", $"{nameof(App)}"); + } + } + + void CurrentDomainFirstChanceException(object? sender, System.Runtime.ExceptionServices.FirstChanceExceptionEventArgs e) + { + Debug.WriteLine($"[ERROR] First chance exception from {sender?.GetType()}: {e.Exception.Message}"); + DebugLog($"First chance exception from {sender?.GetType()}: {e.Exception.Message}"); + if (e.Exception.InnerException != null) + DebugLog($" - InnerException: {e.Exception.InnerException.Message}"); + DebugLog($"First chance exception StackTrace: {Environment.StackTrace}"); + } + + void CurrentDomainUnhandledException(object? sender, System.UnhandledExceptionEventArgs e) + { + Exception? ex = e.ExceptionObject as Exception; + Debug.WriteLine($"[ERROR] Thread exception of type {ex?.GetType()}: {ex}"); + DebugLog($"Thread exception of type {ex?.GetType()}: {ex}"); + } + + void OnUnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs e) + { + if (e.Exception is AggregateException aex) + { + aex?.Flatten().Handle(ex => + { + Debug.WriteLine($"[ERROR] Unobserved task exception: {ex?.Message}"); + DebugLog($"Unobserved task exception: {ex?.Message}"); + return true; + }); + } + e.SetObserved(); // suppress and handle manually + } + #endregion + + #region [Miscellaneous] + public static void CloseAllDialogs() + { + if (App.MainRoot?.XamlRoot == null) { return; } + var openedDialogs = VisualTreeHelper.GetOpenPopupsForXamlRoot(App.MainRoot?.XamlRoot); + foreach (var item in openedDialogs) + { + if (item.Child is ContentDialog dialog) + dialog.Hide(); + } + } + + /// + /// Simplified debug logger for app-wide use. + /// + /// the text to append to the file + public static void DebugLog(string message) + { + try + { + System.IO.File.AppendAllText(System.IO.Path.Combine(System.AppContext.BaseDirectory, "Debug.log"), $"[{TimeStamp}] {message}{Environment.NewLine}"); + } + catch (Exception) + { + Debug.WriteLine($"[{TimeStamp}] {message}"); + } + } + + /// + /// Returns the basic assemblies needed by the application. + /// + public static Dictionary GetReferencedAssemblies(bool addSelf = false) + { + Dictionary values = new Dictionary(); + try + { + var assem = Assembly.GetExecutingAssembly(); + int idx = 0; // to prevent key collisions only + if (addSelf) + values.Add($"{++idx}: {assem.GetName().Name}", assem.GetName().Version ?? new Version()); // add self + IOrderedEnumerable names = assem.GetReferencedAssemblies().OrderBy(o => o.Name); + foreach (var sas in names) + { + values.Add($"{++idx}: {sas.Name}", sas.Version ?? new Version()); + } + } + catch (Exception ex) + { + Debug.WriteLine($"[ERROR] GetReferencedAssemblies: {ex.Message}"); + } + return values; + } + + /// + /// Returns an exhaustive list of all modules involved in the current process. + /// + public static List GetProcessDependencies() + { + List result = new List(); + try + { + string self = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + ".exe"; + System.Diagnostics.ProcessModuleCollection pmc = System.Diagnostics.Process.GetCurrentProcess().Modules; + IOrderedEnumerable pmQuery = pmc + .OfType() + .Where(pt => pt.ModuleMemorySize > 0) + .OrderBy(o => o.ModuleName); + foreach (var item in pmQuery) + { + //if (!item.ModuleName.Contains($"{self}")) + result.Add($"Module name: {item.ModuleName}, {(string.IsNullOrEmpty(item.FileVersionInfo.FileVersion) ? "version unknown" : $"v{item.FileVersionInfo.FileVersion}")}"); + try { item.Dispose(); } + catch { } + } + } + catch (Exception ex) + { + Debug.WriteLine($"[ERROR] ProcessModuleCollection: {ex.Message}"); + } + return result; + } + #endregion +} diff --git a/src/SampleApp/Assets/Info.png b/src/SampleApp/Assets/Info.png new file mode 100644 index 0000000..8957f23 Binary files /dev/null and b/src/SampleApp/Assets/Info.png differ diff --git a/src/SampleApp/Assets/SpinnerRing.png b/src/SampleApp/Assets/SpinnerRing.png new file mode 100644 index 0000000..869cdba Binary files /dev/null and b/src/SampleApp/Assets/SpinnerRing.png differ diff --git a/src/SampleApp/Assets/StoreLogo.ico b/src/SampleApp/Assets/StoreLogo.ico new file mode 100644 index 0000000..66e9c10 Binary files /dev/null and b/src/SampleApp/Assets/StoreLogo.ico differ diff --git a/src/SampleApp/Assets/StoreLogo.png b/src/SampleApp/Assets/StoreLogo.png new file mode 100644 index 0000000..023510d Binary files /dev/null and b/src/SampleApp/Assets/StoreLogo.png differ diff --git a/src/SampleApp/Assets/StoreLogoAlt.ico b/src/SampleApp/Assets/StoreLogoAlt.ico new file mode 100644 index 0000000..ef304fa Binary files /dev/null and b/src/SampleApp/Assets/StoreLogoAlt.ico differ diff --git a/src/SampleApp/Assets/StoreLogoAlt.png b/src/SampleApp/Assets/StoreLogoAlt.png new file mode 100644 index 0000000..056c702 Binary files /dev/null and b/src/SampleApp/Assets/StoreLogoAlt.png differ diff --git a/src/SampleApp/Assets/Warning.png b/src/SampleApp/Assets/Warning.png new file mode 100644 index 0000000..21b3373 Binary files /dev/null and b/src/SampleApp/Assets/Warning.png differ diff --git a/src/SampleApp/Assets/mtns.csv b/src/SampleApp/Assets/mtns.csv index 8028212..3722c9a 100644 --- a/src/SampleApp/Assets/mtns.csv +++ b/src/SampleApp/Assets/mtns.csv @@ -5,7 +5,7 @@ 5,Makalu,8485,Mahalangur Himalaya,27d53m23sN 87d5m20sE,2386,Mount Everest,05/15/1955,45 (52) 6,Cho Oyu,8188,Mahalangur Himalaya,28d05m39sN 86d39m39sE,2340,Mount Everest,10/19/1954,79 (28) 7,Dhaulagiri I,8167,Dhaulagiri Himalaya,28d41m48sN 83d29m35sE,3357,K2,05/13/1960,51 (39) -8,Manaslu,8163,Manaslu Himalaya,28d33m00sN 84d33m35sE,3092,Cho Oyu,05/9/1956,49 (45) +8,Manaslu,8163,Manaslu Himalaya,28d33m00sN 84d33m35sE,3092,Cho Oyu,05/09/1956,49 (45) 9,Nanga Parbat,8126,Nanga Parbat Himalaya,35d14m14sN 74d35m21sE,4608,Dhaulagiri,07/03/1953,52 (67) 10,Annapurna I,8091,Annapurna Himalaya,28d35m44sN 83d49m13sE,2984,Cho Oyu,06/03/1950,36 (47) 11,Gasherbrum I,8080,Baltoro Karakoram,35d43m28sN 76d41m47sE,2155,K2,01/01/1958,31 (16) diff --git a/src/SampleApp/DataGridDataItem.cs b/src/SampleApp/DataGridDataItem.cs index 028ac3e..d511d23 100644 --- a/src/SampleApp/DataGridDataItem.cs +++ b/src/SampleApp/DataGridDataItem.cs @@ -1,255 +1,223 @@ -// 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.ComponentModel; -using System.Runtime.CompilerServices; - -namespace SampleApp; - -#nullable disable - -public class DataGridDataItem : INotifyDataErrorInfo, IComparable, INotifyPropertyChanged -{ - private Dictionary> _errors = new Dictionary>(); - private uint _rank; - private string _mountain; - private uint _height; - private string _range; - private string _parentMountain; - private string coordinates; - private uint prominence; - private DateTimeOffset first_ascent; - private string ascents; - - public event EventHandler ErrorsChanged; - public event PropertyChangedEventHandler PropertyChanged; - - public uint Rank - { - get - { - return _rank; - } - - set - { - if (_rank != value) - { - _rank = value; - OnPropertyChanged(); - } - } - } - - public string Mountain - { - get - { - return _mountain; - } - - set - { - if (_mountain != value) - { - _mountain = value; - - bool isMountainValid = !_errors.ContainsKey("Mountain"); - if (_mountain == string.Empty && isMountainValid) - { - List errors = new List(); - errors.Add("Mountain name cannot be empty"); - _errors.Add("Mountain", errors); - ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Mountain")); - } - else if (_mountain != string.Empty && !isMountainValid) - { - _errors.Remove("Mountain"); - ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Mountain")); - } - - OnPropertyChanged(); - } - } - } - - public uint Height_m - { - get - { - return _height; - } - - set - { - if (_height != value) - { - _height = value; - OnPropertyChanged(); - } - } - } - - public string Range - { - get - { - return _range; - } - - set - { - if (_range != value) - { - _range = value; - - bool isRangeValid = !_errors.ContainsKey("Range"); - if (_range == string.Empty && isRangeValid) - { - List errors = new List(); - errors.Add("Range name cannot be empty"); - _errors.Add("Range", errors); - ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Range")); - } - else if (_range != string.Empty && !isRangeValid) - { - _errors.Remove("Range"); - ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Range")); - } - - OnPropertyChanged(); - } - } - } - - public string Parent_mountain - { - get - { - return _parentMountain; - } - - set - { - if (_parentMountain != value) - { - _parentMountain = value; - - bool isParentValid = !_errors.ContainsKey("Parent_mountain"); - if (_parentMountain == string.Empty && isParentValid) - { - List errors = new List(); - errors.Add("Parent_mountain name cannot be empty"); - _errors.Add("Parent_mountain", errors); - ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Parent_mountain")); - } - else if (_parentMountain != string.Empty && !isParentValid) - { - _errors.Remove("Parent_mountain"); - ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Parent_mountain")); - } - - OnPropertyChanged(); - } - } - } - - public string Coordinates - { - get => coordinates; - set - { - if (coordinates != value) - { - coordinates = value; - OnPropertyChanged(); - } - } - } - - public uint Prominence - { - get => prominence; - set - { - if (prominence != value) - { - prominence = value; - OnPropertyChanged(); - } - } - } - - // You need to use DateTimeOffset to get proper binding to the CalendarDatePicker control, DateTime won't work. - public DateTimeOffset First_ascent - { - get => first_ascent; set - { - if (first_ascent != value) - { - first_ascent = value; - OnPropertyChanged(); - } - } - } - - public string Ascents - { - get => ascents; set - { - if (ascents != value) - { - ascents = value; - OnPropertyChanged(); - } - } - } - - bool INotifyDataErrorInfo.HasErrors - { - get - { - return _errors.Keys.Count > 0; - } - } - - IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName) - { - if (propertyName == null) - { - propertyName = string.Empty; - } - - if (_errors.ContainsKey(propertyName)) - { - return _errors[propertyName]; - } - else - { - return null; - } - } - - int IComparable.CompareTo(object obj) - { - int lnCompare = Range.CompareTo((obj as DataGridDataItem).Range); - - if (lnCompare == 0) - { - return Parent_mountain.CompareTo((obj as DataGridDataItem).Parent_mountain); - } - else - { - return lnCompare; - } - } - - private void OnPropertyChanged([CallerMemberName] string propertyName = null) - { - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); - } +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.CompilerServices; + +namespace SampleApp; + +#nullable disable + +public class DataGridDataItem : INotifyDataErrorInfo, IComparable, INotifyPropertyChanged +{ + string _mountain; + string _range; + string _parentMountain; + string _coordinates; + string _ascents; + uint _rank; + uint _height; + uint _prominence; + DateTimeOffset _firstAscent; + Dictionary> _errors = new Dictionary>(); + + public event EventHandler ErrorsChanged; + public event PropertyChangedEventHandler PropertyChanged; + + void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + if (!string.IsNullOrEmpty(propertyName)) + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + public uint Rank + { + get => _rank; + set + { + if (_rank != value) + { + _rank = value; + OnPropertyChanged(); + } + } + } + + public string Mountain + { + get => _mountain; + set + { + if (_mountain != value) + { + _mountain = value; + + bool isMountainValid = !_errors.ContainsKey("Mountain"); + if (_mountain == string.Empty && isMountainValid) + { + List errors = new List(); + errors.Add("Mountain name cannot be empty"); + _errors.Add("Mountain", errors); + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Mountain")); + } + else if (_mountain != string.Empty && !isMountainValid) + { + _errors.Remove("Mountain"); + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Mountain")); + } + + OnPropertyChanged(); + } + } + } + + public uint Height_m + { + get => _height; + set + { + if (_height != value) + { + _height = value; + OnPropertyChanged(); + } + } + } + + public string Range + { + get => _range; + set + { + if (_range != value) + { + _range = value; + + bool isRangeValid = !_errors.ContainsKey("Range"); + if (_range == string.Empty && isRangeValid) + { + List errors = new List(); + errors.Add("Range name cannot be empty"); + _errors.Add("Range", errors); + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Range")); + } + else if (_range != string.Empty && !isRangeValid) + { + _errors.Remove("Range"); + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Range")); + } + + OnPropertyChanged(); + } + } + } + + public string Parent_mountain + { + get => _parentMountain; + set + { + if (_parentMountain != value) + { + _parentMountain = value; + + bool isParentValid = !_errors.ContainsKey("Parent_mountain"); + if (_parentMountain == string.Empty && isParentValid) + { + List errors = new List(); + errors.Add("Parent_mountain name cannot be empty"); + _errors.Add("Parent_mountain", errors); + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Parent_mountain")); + } + else if (_parentMountain != string.Empty && !isParentValid) + { + _errors.Remove("Parent_mountain"); + ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs("Parent_mountain")); + } + + OnPropertyChanged(); + } + } + } + + public string Coordinates + { + get => _coordinates; + set + { + if (_coordinates != value) + { + _coordinates = value; + OnPropertyChanged(); + } + } + } + + public uint Prominence + { + get => _prominence; + set + { + if (_prominence != value) + { + _prominence = value; + OnPropertyChanged(); + } + } + } + + /// + /// You need to use DateTimeOffset to get proper binding to the CalendarDatePicker control, DateTime won't work. + /// + public DateTimeOffset First_ascent + { + get => _firstAscent; + set + { + if (_firstAscent != value) + { + _firstAscent = value; + OnPropertyChanged(); + } + } + } + + public string Ascents + { + get => _ascents; + set + { + if (_ascents != value) + { + _ascents = value; + OnPropertyChanged(); + } + } + } + + bool INotifyDataErrorInfo.HasErrors + { + get => _errors.Keys.Count > 0; + } + + IEnumerable INotifyDataErrorInfo.GetErrors(string propertyName) + { + if (propertyName == null) + propertyName = string.Empty; + + if (_errors.ContainsKey(propertyName)) + return _errors[propertyName]; + else + return null; + } + + int IComparable.CompareTo(object obj) + { + int lnCompare = Range.CompareTo((obj as DataGridDataItem).Range); + + if (lnCompare == 0) + return Parent_mountain.CompareTo((obj as DataGridDataItem).Parent_mountain); + else + return lnCompare; + } } \ No newline at end of file diff --git a/src/SampleApp/Helpers/ObservableCollectionEx.cs b/src/SampleApp/Helpers/ObservableCollectionEx.cs new file mode 100644 index 0000000..a5d97bd --- /dev/null +++ b/src/SampleApp/Helpers/ObservableCollectionEx.cs @@ -0,0 +1,57 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; + +namespace SampleApp.Helpers; + +/// +/// Extends the functionality of the type. +/// Provides the ability to add a range of items and only fire the collection changed event one time. +/// https://github.com/AndrewKeepCoding/ObservableCollectionExSampleApp/blob/main/ObservableCollectionExSampleApp/ObservableCollectionEx.cs +/// +/// data type for the collection +public class ObservableCollectionEx : ObservableCollection +{ + public void AddRange(IEnumerable collection) + { + CheckReentrancy(); // from the System.Collections.ObjectModel.ObservableCollection class + + // Is there anything to add? + if (collection.Any() is false) + return; + + List itemsList = (List)Items; + itemsList.AddRange(collection); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(action: NotifyCollectionChangedAction.Add, changedItems: itemsList, startingIndex: itemsList.Count - 1)); + } + + public void AddRange(IList collection) + { + CheckReentrancy(); // from the System.Collections.ObjectModel.ObservableCollection class + + // Is there anything to add? + if (collection.Any() is false) + return; + + List itemsList = (List)Items; + itemsList.AddRange(collection); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(action: NotifyCollectionChangedAction.Add, changedItems: itemsList, startingIndex: itemsList.Count - 1)); + } + + public void AddRange(ICollection collection) + { + CheckReentrancy(); // from the System.Collections.ObjectModel.ObservableCollection class + + // Is there anything to add? + if (collection.Any() is false) + return; + + List itemsList = (List)Items; + itemsList.AddRange(collection); + + OnCollectionChanged(new NotifyCollectionChangedEventArgs(action: NotifyCollectionChangedAction.Add, changedItems: itemsList, startingIndex: itemsList.Count - 1)); + } +} \ No newline at end of file diff --git a/src/SampleApp/Helpers/VisualTreeHelperExtensions.cs b/src/SampleApp/Helpers/VisualTreeHelperExtensions.cs new file mode 100644 index 0000000..58205a5 --- /dev/null +++ b/src/SampleApp/Helpers/VisualTreeHelperExtensions.cs @@ -0,0 +1,217 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Media; + +using Windows.Foundation; + +namespace SampleApp.Helpers; + +public static class VisualTreeHelperExtensions +{ + public static T? GetFirstDescendantOfType(this DependencyObject start) where T : DependencyObject + { + return start.GetDescendantsOfType().FirstOrDefault(); + } + + public static IEnumerable GetDescendantsOfType(this DependencyObject start) where T : DependencyObject + { + return start.GetDescendants().OfType(); + } + + public static IEnumerable GetDescendants(this DependencyObject start) + { + var queue = new Queue(); + var count = VisualTreeHelper.GetChildrenCount(start); + + for (int i = 0; i < count; i++) + { + var child = VisualTreeHelper.GetChild(start, i); + yield return child; + queue.Enqueue(child); + } + + while (queue.Count > 0) + { + var parent = queue.Dequeue(); + var count2 = VisualTreeHelper.GetChildrenCount(parent); + + for (int i = 0; i < count2; i++) + { + var child = VisualTreeHelper.GetChild(parent, i); + yield return child; + queue.Enqueue(child); + } + } + } + + public static T? GetFirstAncestorOfType(this DependencyObject start) where T : DependencyObject + { + return start.GetAncestorsOfType().FirstOrDefault(); + } + + public static IEnumerable GetAncestorsOfType(this DependencyObject start) where T : DependencyObject + { + return start.GetAncestors().OfType(); + } + + public static IEnumerable GetAncestors(this DependencyObject start) + { + var parent = VisualTreeHelper.GetParent(start); + + while (parent != null) + { + yield return parent; + parent = VisualTreeHelper.GetParent(parent); + } + } + + public static bool IsInVisualTree(this DependencyObject dob) + { + return Window.Current.Content != null && dob.GetAncestors().Contains(Window.Current.Content); + } + + public static Rect GetBoundingRect(this FrameworkElement dob, FrameworkElement? relativeTo = null) + { + if (relativeTo == null) + { + relativeTo = Window.Current.Content as FrameworkElement; + } + + if (relativeTo == null) + { + throw new InvalidOperationException("Element not in visual tree."); + } + + if (dob == relativeTo) + return new Rect(0, 0, relativeTo.ActualWidth, relativeTo.ActualHeight); + + var ancestors = dob.GetAncestors().ToArray(); + + if (!ancestors.Contains(relativeTo)) + { + throw new InvalidOperationException("Element not in visual tree."); + } + + var pos = + dob + .TransformToVisual(relativeTo) + .TransformPoint(new Point()); + var pos2 = + dob + .TransformToVisual(relativeTo) + .TransformPoint( + new Point( + dob.ActualWidth, + dob.ActualHeight)); + + return new Rect(pos, pos2); + } + + public static IEnumerable GetHierarchyFromUIElement(this Type element) + { + if (element.GetTypeInfo().IsSubclassOf(typeof(UIElement)) != true) + { + yield break; + } + + Type? current = element; + + while (current != null && current != typeof(UIElement)) + { + yield return current; + current = current.GetTypeInfo().BaseType; + } + } + + public static TypeInfo GetTypeInfo(this Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (type is IReflectableType reflectableType) + return reflectableType.GetTypeInfo(); + + return new TypeDelegator(type); + } + + public static T? FindVisualChildByType(this DependencyObject element) where T : DependencyObject + { + if (element == null) + return null; + + if (element is T elementAsT) + return elementAsT; + + int childrenCount = VisualTreeHelper.GetChildrenCount(element); + for (int i = 0; i < childrenCount; i++) + { + var result = VisualTreeHelper.GetChild(element, i).FindVisualChildByType(); + if (result != null) + { + return result; + } + } + + return null; + } + + public static FrameworkElement? FindVisualChildByName(this DependencyObject element, string name) + { + if (element == null || string.IsNullOrWhiteSpace(name)) + return null; + + if (element is FrameworkElement elementAsFE && elementAsFE.Name == name) + return elementAsFE; + + int childrenCount = VisualTreeHelper.GetChildrenCount(element); + for (int i = 0; i < childrenCount; i++) + { + var result = VisualTreeHelper.GetChild(element, i).FindVisualChildByName(name); + if (result != null) + { + return result; + } + } + + return null; + } + + public static T? FindVisualParentByType(this DependencyObject element) where T : DependencyObject + { + if (element is null) + return null; + + return element is T elementAsT + ? elementAsT + : VisualTreeHelper.GetParent(element).FindVisualParentByType(); + } + + public static FrameworkElement? FindVisualParentByName(this DependencyObject element, string name) + { + if (element is null || string.IsNullOrWhiteSpace(name)) + return null; + + if (element is FrameworkElement elementAsFE && elementAsFE.Name == name) + return elementAsFE; + + return VisualTreeHelper.GetParent(element).FindVisualParentByName(name); + } + + /// + /// foreach (UIElement element in navigationViewItem.GetChildren()) { _ = results.Add(element); } + /// + public static IEnumerable GetChildren(this UIElement parent) + { + for (int i = 0; i < VisualTreeHelper.GetChildrenCount(parent); i++) + { + if (VisualTreeHelper.GetChild(parent, i) is UIElement child) + { + yield return child; + } + } + } +} diff --git a/src/SampleApp/MainWindow.xaml b/src/SampleApp/MainWindow.xaml index b69129e..87654bb 100644 --- a/src/SampleApp/MainWindow.xaml +++ b/src/SampleApp/MainWindow.xaml @@ -1,63 +1,147 @@ - - - - - - - - - - - - -