diff --git a/samples/WpfDemoLib/ViewModels/MainViewModel.cs b/samples/WpfDemoLib/ViewModels/MainViewModel.cs index 563e469..8bc218e 100644 --- a/samples/WpfDemoLib/ViewModels/MainViewModel.cs +++ b/samples/WpfDemoLib/ViewModels/MainViewModel.cs @@ -11,9 +11,6 @@ namespace WpfDemoLib.ViewModels; public sealed class MainViewModel : ObservableObject { - public const string NotificationWarningIconResourceName = - "pack://application:,,,/WpfDemoLib;component/assets/images/icons8-notification-warning-32.png"; - private string? _fileDialogResults; private string? _secondWindowResult; @@ -64,6 +61,7 @@ public MainViewModel( public IRelayCommand ShowDialogOpenFileCommand { get; set; } public IRelayCommand ShowDialogSaveFileCommand { get; set; } public IRelayCommand ShowDialogOpenFolderCommand { get; set; } + public IRelayCommand ShowNotificationCommand { get; set; } public string? FileDialogResults { get => _fileDialogResults; diff --git a/samples/WpfDemoLib/ViewModels/Pages/NotificationViewModel.cs b/samples/WpfDemoLib/ViewModels/Pages/NotificationViewModel.cs new file mode 100644 index 0000000..4304776 --- /dev/null +++ b/samples/WpfDemoLib/ViewModels/Pages/NotificationViewModel.cs @@ -0,0 +1,61 @@ +using System.Windows.Input; +using System.Windows.Media.Imaging; + +using dosymep.SimpleServices; + +using WpfDemoLib.ComponentModel; +using WpfDemoLib.Factories; + +namespace WpfDemoLib.ViewModels.Pages; + +public sealed class NotificationViewModel : ObservableObject { + public const string NotificationWarningIconResourceName = + "pack://application:,,,/WpfDemoLib;component/assets/images/icons8-notification-warning-32.png"; + + public INotificationService NotificationService { get; } + + public NotificationViewModel(ICommandFactory commandFactory, INotificationService notificationService) { + NotificationService = notificationService; + + ShowNotificationCommand = commandFactory.CreateAsync(ShowNotificationAsync); + ShowFatalNotificationCommand = commandFactory.CreateAsync(ShowFatalNotificationAsync); + ShowWarningNotificationCommand = commandFactory.CreateAsync(ShowWarningNotificationAsync); + } + + public ICommand ShowNotificationCommand { get; } + public ICommand ShowFatalNotificationCommand { get; } + public ICommand ShowWarningNotificationCommand { get; } + + private int _count = 0; + + private async Task ShowNotificationAsync() { + INotification notification = NotificationService.CreateNotification( + title: "Title " + _count++, + body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + footer: "IronPython", + author: "dosymep", + imageSource: new BitmapImage(new Uri(NotificationWarningIconResourceName, UriKind.Absolute))); + + await notification.ShowAsync(); + } + + private async Task ShowFatalNotificationAsync() { + INotification notification = NotificationService.CreateFatalNotification( + title: "IronPython " + _count++, + body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + author: "dosymep", + imageSource: new BitmapImage(new Uri(NotificationWarningIconResourceName, UriKind.Absolute))); + + await notification.ShowAsync(); + } + + private async Task ShowWarningNotificationAsync() { + INotification notification = NotificationService.CreateWarningNotification( + title: "IronPython " + _count++, + body: "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + author: "dosymep", + imageSource: new BitmapImage(new Uri(NotificationWarningIconResourceName, UriKind.Absolute))); + + await notification.ShowAsync(); + } +} \ No newline at end of file diff --git a/samples/WpfUIDemoApp/App.xaml.cs b/samples/WpfUIDemoApp/App.xaml.cs index f9c53b6..7ad93b7 100644 --- a/samples/WpfUIDemoApp/App.xaml.cs +++ b/samples/WpfUIDemoApp/App.xaml.cs @@ -6,6 +6,7 @@ using dosymep.SimpleServices; using dosymep.WpfCore.Ninject; +using dosymep.WpfCore.SimpleServices; using dosymep.WpfUI.Core.Ninject; using Ninject; @@ -18,9 +19,11 @@ using WpfDemoLib.Input.Interfaces; using WpfDemoLib.Services; using WpfDemoLib.ViewModels; +using WpfDemoLib.ViewModels.Pages; using WpfUIDemoApp.Factories; using WpfUIDemoApp.Views; +using WpfUIDemoApp.Views.Pages; namespace WpfUIDemoApp; @@ -30,61 +33,66 @@ namespace WpfUIDemoApp; public partial class App { public const string LocalizationResourceName = "/WpfUIDemoApp;component/assets/localizations/language.xaml"; - + public const string NotificationIconResourceName = "pack://application:,,,/WpfUIDemoApp;component/assets/images/icons8-notification-32.png"; - + public const string NotificationWarningIconResourceName = "pack://application:,,,/WpfUIDemoApp;component/assets/images/icons8-notification-warning-32.png"; - + private IKernel? _kernel; - + protected override void OnStartup(StartupEventArgs e) { base.OnStartup(e); - + _kernel = new StandardKernel(); - + _kernel.Bind().To(); - + _kernel.UseWpfDispatcher(); - + _kernel.UseWpfWindowsLanguage(); _kernel.UseWpfLocalization(LocalizationResourceName, CultureInfo.GetCultureInfo("ru-RU")); - + _kernel.UseWpfWindowsTheme(); _kernel.UseWpfUIThemeUpdater(); - + ILocalizationService localizationService = _kernel.Get(); - + _kernel.UseWpfUIMessageBox(); - + _kernel.UseWpfUINotifications(); + _kernel.UseWpfUIProgressDialog( displayTitleFormat: localizationService.GetLocalizedString("ProgressDialog.Content")); - + _kernel.UseWpfOpenFileDialog( title: localizationService.GetLocalizedString("OpenFileDialog.Title")); - + _kernel.UseWpfSaveFileDialog( title: localizationService.GetLocalizedString("SaveFileDialog.Title")); - + _kernel.UseWpfOpenFolderDialog( title: localizationService.GetLocalizedString("OpenFolderDialog.Title")); - + _kernel.Bind().To(); _kernel.Bind() .ToMethod(c => new SecondViewFactory(() => c.Kernel.Get())); - + _kernel.Bind() .To(); - + _kernel.Bind().ToSelf(); _kernel.Bind().ToSelf(); - + + + _kernel.Bind().ToSelf().InSingletonScope(); + _kernel.Bind().ToSelf().InSingletonScope(); + _kernel.BindMainWindow(); - + _kernel.Get().Show(); } - + protected override void OnExit(ExitEventArgs e) { base.OnExit(e); _kernel?.Dispose(); diff --git a/samples/WpfUIDemoApp/MainWindow.xaml b/samples/WpfUIDemoApp/MainWindow.xaml index 19f4cda..7ebb12e 100644 --- a/samples/WpfUIDemoApp/MainWindow.xaml +++ b/samples/WpfUIDemoApp/MainWindow.xaml @@ -15,7 +15,7 @@ xmlns:behaviors="clr-namespace:dosymep.WpfCore.Behaviors;assembly=dosymep.WpfCore" xmlns:viewModels="clr-namespace:WpfDemoLib.ViewModels;assembly=WpfDemoLib" xmlns:pages="clr-namespace:WpfUIDemoApp.Views.Pages" - + Height="600" Width="1000" @@ -37,7 +37,7 @@ - + + + diff --git a/samples/WpfUIDemoApp/Views/Pages/NotificationPage.xaml b/samples/WpfUIDemoApp/Views/Pages/NotificationPage.xaml new file mode 100644 index 0000000..eb8ddfd --- /dev/null +++ b/samples/WpfUIDemoApp/Views/Pages/NotificationPage.xaml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/samples/WpfUIDemoApp/Views/Pages/NotificationPage.xaml.cs b/samples/WpfUIDemoApp/Views/Pages/NotificationPage.xaml.cs new file mode 100644 index 0000000..8be3830 --- /dev/null +++ b/samples/WpfUIDemoApp/Views/Pages/NotificationPage.xaml.cs @@ -0,0 +1,19 @@ +using System.Windows.Controls; + +using dosymep.SimpleServices; + +using WpfDemoLib.ViewModels; +using WpfDemoLib.ViewModels.Pages; + +namespace WpfUIDemoApp.Views.Pages; + +public partial class NotificationPage { + public NotificationPage( + IHasTheme hasTheme, + IHasLocalization hasLocalization, + NotificationViewModel notificationViewModel) + : base(hasTheme, hasLocalization) { + InitializeComponent(); + DataContext = notificationViewModel; + } +} \ No newline at end of file diff --git a/samples/WpfUIDemoApp/Views/Pages/WpfUIPlatformPage.cs b/samples/WpfUIDemoApp/Views/Pages/WpfUIPlatformPage.cs index ad3e9b3..661736e 100644 --- a/samples/WpfUIDemoApp/Views/Pages/WpfUIPlatformPage.cs +++ b/samples/WpfUIDemoApp/Views/Pages/WpfUIPlatformPage.cs @@ -8,11 +8,11 @@ namespace WpfUIDemoApp.Views.Pages { public class WpfUIPlatformPage : Page, IHasTheme, IHasLocalization { - public event Action? ThemeChanged; - public event Action? LanguageChanged; - - private readonly IHasTheme? _hasTheme; - private readonly IHasLocalization? _hasLocalization; + public event Action ThemeChanged = null!; + public event Action LanguageChanged = null!; + + private readonly IHasTheme _hasTheme = null!; + private readonly IHasLocalization _hasLocalization = null!; public WpfUIPlatformPage() { } @@ -24,7 +24,7 @@ public WpfUIPlatformPage( _hasTheme.ThemeChanged += _ => ThemeChanged?.Invoke(_); _hasLocalization.LanguageChanged += _ => LanguageChanged?.Invoke(_); - + Interaction.GetBehaviors(this).Add(new WpfThemeBehavior()); Interaction.GetBehaviors(this).Add(new WpfLocalizationBehavior()); } @@ -35,4 +35,4 @@ public WpfUIPlatformPage( public UIThemes HostTheme => _hasTheme.HostTheme; public CultureInfo HostLanguage => _hasLocalization.HostLanguage; } -} +} \ No newline at end of file diff --git a/samples/WpfUIDemoApp/assets/localizations/language.en-US.xaml b/samples/WpfUIDemoApp/assets/localizations/language.en-US.xaml index 09717da..ee22738 100644 --- a/samples/WpfUIDemoApp/assets/localizations/language.en-US.xaml +++ b/samples/WpfUIDemoApp/assets/localizations/language.en-US.xaml @@ -41,6 +41,11 @@ Save file Choose folder + Notifications + Show notification + Show fatal notification + Show warning notification + Dark Light \ No newline at end of file diff --git a/samples/WpfUIDemoApp/assets/localizations/language.ru-RU.xaml b/samples/WpfUIDemoApp/assets/localizations/language.ru-RU.xaml index 2041d31..ad061ca 100644 --- a/samples/WpfUIDemoApp/assets/localizations/language.ru-RU.xaml +++ b/samples/WpfUIDemoApp/assets/localizations/language.ru-RU.xaml @@ -46,6 +46,11 @@ Сохранить файл Выбрать папку + Уведомления + Показать уведомление + Показать ошибку + Показать предупреждение + Темная Светлая \ No newline at end of file diff --git a/src/dosymep.WpfCore/Animations/NotificationAnimations.xaml b/src/dosymep.WpfCore/Animations/NotificationAnimations.xaml new file mode 100644 index 0000000..1a93040 --- /dev/null +++ b/src/dosymep.WpfCore/Animations/NotificationAnimations.xaml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dosymep.WpfCore/Behaviors/WpfNotificationWindowBehavior.cs b/src/dosymep.WpfCore/Behaviors/WpfNotificationWindowBehavior.cs new file mode 100644 index 0000000..59cf1fa --- /dev/null +++ b/src/dosymep.WpfCore/Behaviors/WpfNotificationWindowBehavior.cs @@ -0,0 +1,239 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Threading; + +using dosymep.WpfCore.SimpleServices; + +using Microsoft.Xaml.Behaviors; + +namespace dosymep.WpfCore.Behaviors; + +/// +/// Поведение, которое применяет анимацию скольжения появления окна. +/// +public sealed class WpfNotificationWindowBehavior : Behavior { + /// + /// Определяет, на каком мониторе будет отображаться уведомление. + /// + public static readonly DependencyProperty NotificationScreenProperty = DependencyProperty.Register( + nameof(NotificationScreen), + typeof(NotificationScreen), + typeof(WpfNotificationWindowBehavior), + new PropertyMetadata(default(NotificationScreen))); + + /// + /// Позиция уведомления на экране. + /// + public static readonly DependencyProperty NotificationPositionProperty = DependencyProperty.Register( + nameof(NotificationPosition), + typeof(NotificationPosition), + typeof(WpfNotificationWindowBehavior), + new PropertyMetadata(default(NotificationPosition))); + + /// + /// Список открытых окон уведомлений. + /// + public static readonly DependencyProperty WindowStackProperty = DependencyProperty.Register( + nameof(WindowStack), + typeof(ObservableCollection), + typeof(WpfNotificationWindowBehavior), + new PropertyMetadata(default(ObservableCollection))); + + /// + /// Идентифицирует свойство зависимости BottomOffset, + /// которое определяет расстояние от нижнего края экрана до нижнего края окна при анимации скольжения. + /// + public static readonly DependencyProperty OffsetProperty = DependencyProperty.Register( + nameof(Offset), + typeof(double), + typeof(WpfNotificationWindowBehavior), + new PropertyMetadata(10.0)); + + private bool _isClosed; + + private Storyboard? _showing; + private Storyboard? _closing; + private Storyboard? _autoClosing; + private Storyboard? _positioning; + + /// + protected override void OnAttached() { + AssociatedObject.Loaded += OnWindowLoaded; + AssociatedObject.RenderTransform = new TranslateTransform(); + + ResourceDictionary resources = new() { + Source = new Uri("pack://application:,,,/dosymep.WpfCore;component/Animations/NotificationAnimations.xaml") + }; + + _showing = (Storyboard) resources["Showing"]; + Storyboard.SetTarget(_showing, AssociatedObject); + + _closing = (Storyboard) resources["Closing"]; + Storyboard.SetTarget(_closing, AssociatedObject); + + _autoClosing = (Storyboard) resources["AutoClosing"]; + Storyboard.SetTarget(_autoClosing, AssociatedObject); + + _positioning = (Storyboard) resources["Positioning"]; + Storyboard.SetTarget(_positioning, AssociatedObject); + + _closing.Completed += ClosingOnCompleted; + _autoClosing.Completed += ClosingOnCompleted; + } + + /// + /// Список открытых окон уведомлений. + /// + public ObservableCollection WindowStack { + get => (ObservableCollection) GetValue(WindowStackProperty); + set => SetValue(WindowStackProperty, value); + } + + /// + /// Определяет, на каком мониторе будет отображаться уведомление. + /// + public NotificationScreen NotificationScreen { + get => (NotificationScreen) GetValue(NotificationScreenProperty); + set => SetValue(NotificationScreenProperty, value); + } + + /// + /// Позиция уведомления на экране. + /// + public NotificationPosition NotificationPosition { + get => (NotificationPosition) GetValue(NotificationPositionProperty); + set => SetValue(NotificationPositionProperty, value); + } + + /// + /// Определяет расстояние от нижнего края экрана до нижнего края окна при анимации скольжения. + /// + public double Offset { + get => (double) GetValue(OffsetProperty); + set => SetValue(OffsetProperty, value); + } + + /// + protected override void OnDetaching() { + AssociatedObject.Loaded -= OnWindowLoaded; + } + + private void OnWindowLoaded(object sender, RoutedEventArgs e) { + OnShowing(); + } + + private void ClosingOnCompleted(object sender, EventArgs e) { + AssociatedObject.Close(); + + int windowIndex = WindowStack.IndexOf(AssociatedObject); + WindowStack.RemoveAt(windowIndex); + + if(_closing is not null) { + _closing.Completed -= ClosingOnCompleted; + } + + if(_autoClosing is not null) { + _autoClosing.Completed -= ClosingOnCompleted; + } + + + int sign = GetSign(); + for(int index = 0; index < WindowStack.Count; index++) { + Window window = WindowStack[index]; + if(index > (windowIndex - 1)) { + window.Top += sign * AssociatedObject.ActualHeight + sign * Offset; + } + } + } + + /// + /// Запускает анимацию показа окна. + /// + public void OnShowing() { + WindowStack.Add(AssociatedObject); + + if(NotificationPosition == NotificationPosition.TopRight) { + SetTopPosition(AssociatedObject); + } else if(NotificationPosition == NotificationPosition.BottomRight) { + SetBottomPosition(AssociatedObject); + } else { + throw new NotSupportedException($"Position {NotificationPosition} is not supported."); + } + + _showing?.Begin(AssociatedObject); + } + + private void SetBottomPosition(Window window) { + var screen = GetMainScreen(); + var dpiScale = VisualTreeHelper.GetDpi(window); + + window.Top = screen.WorkingArea.Top / dpiScale.DpiScaleY + + screen.WorkingArea.Height / dpiScale.DpiScaleY - + WindowStack.Count * window.ActualHeight - WindowStack.Count * Offset; + + window.Left = screen.WorkingArea.Left / dpiScale.DpiScaleX + + screen.WorkingArea.Width / dpiScale.DpiScaleX - window.ActualWidth; + } + + /// + /// Запускает анимацию закрытия окна. + /// + public void OnClosing() { + if(_closing is not null && !_isClosed) { + _closing.Begin(AssociatedObject); + } + + _isClosed = true; + } + + /// + /// Запускает анимацию закрытия окна по времени. + /// + public void OnAutoClosing() { + if(_autoClosing is not null && !_isClosed) { + _autoClosing.Begin(AssociatedObject); + } + + _isClosed = true; + } + + private int GetSign() { + if(NotificationPosition == NotificationPosition.TopRight) { + return -1; + } + + if(NotificationPosition == NotificationPosition.BottomRight) { + return 1; + } + + throw new NotSupportedException($"Position {NotificationPosition} is not supported."); + } + + private Screen GetMainScreen() { + if(NotificationScreen == NotificationScreen.Primary) { + return Screen.PrimaryScreen; + } + + if(NotificationScreen == NotificationScreen.ApplicationWindow) { + return Screen.FromHandle(new WindowInteropHelper(AssociatedObject).Handle); + } + + throw new NotSupportedException($"Screen {NotificationScreen} is not supported."); + } + + private void SetTopPosition(Window window) { + var screen = GetMainScreen(); + var dpiScale = VisualTreeHelper.GetDpi(window); + + int startBarHeight = screen.Bounds.Height - screen.WorkingArea.Height; + + window.Top = -startBarHeight + WindowStack.Count * window.ActualHeight + WindowStack.Count * Offset; + + window.Left = screen.WorkingArea.Left / dpiScale.DpiScaleX + + screen.WorkingArea.Width / dpiScale.DpiScaleX - window.ActualWidth; + } +} \ No newline at end of file diff --git a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs index a9e214d..f714dc2 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs @@ -11,6 +11,8 @@ using dosymep.SimpleServices; using dosymep.WpfCore.MarkupExtensions.Internal; +using Binding = System.Windows.Data.Binding; + namespace dosymep.WpfCore.MarkupExtensions; /// diff --git a/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs index 910aafc..547d416 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs @@ -11,6 +11,8 @@ using dosymep.WpfCore.Converters; using dosymep.WpfCore.MarkupExtensions.Internal; +using Binding = System.Windows.Data.Binding; + namespace dosymep.WpfCore.MarkupExtensions; /// diff --git a/src/dosymep.WpfCore/SimpleServices/NotificationPosition.cs b/src/dosymep.WpfCore/SimpleServices/NotificationPosition.cs new file mode 100644 index 0000000..a62d01b --- /dev/null +++ b/src/dosymep.WpfCore/SimpleServices/NotificationPosition.cs @@ -0,0 +1,16 @@ +namespace dosymep.WpfCore.SimpleServices; + +/// +/// Определяет возможные позиции отображения уведомлений. +/// +public enum NotificationPosition { + /// + /// Сверху справа. + /// + TopRight, + + /// + /// Снизу справа. + /// + BottomRight, +} \ No newline at end of file diff --git a/src/dosymep.WpfCore/SimpleServices/NotificationScreen.cs b/src/dosymep.WpfCore/SimpleServices/NotificationScreen.cs new file mode 100644 index 0000000..39fcbe9 --- /dev/null +++ b/src/dosymep.WpfCore/SimpleServices/NotificationScreen.cs @@ -0,0 +1,16 @@ +namespace dosymep.WpfCore.SimpleServices; + +/// +/// Перечисление, представляющее различные экраны для отображения уведомлений. +/// +public enum NotificationScreen { + /// + /// Основной экран. + /// + Primary, + + /// + /// Экран, где расположено окно. + /// + ApplicationWindow, +} \ No newline at end of file diff --git a/src/dosymep.WpfCore/SimpleServices/WpfOpenFileDialogService.cs b/src/dosymep.WpfCore/SimpleServices/WpfOpenFileDialogService.cs index e184896..66e2bab 100644 --- a/src/dosymep.WpfCore/SimpleServices/WpfOpenFileDialogService.cs +++ b/src/dosymep.WpfCore/SimpleServices/WpfOpenFileDialogService.cs @@ -5,6 +5,8 @@ using Microsoft.Win32; +using OpenFileDialog = Microsoft.Win32.OpenFileDialog; + namespace dosymep.WpfCore.SimpleServices; /// diff --git a/src/dosymep.WpfCore/SimpleServices/WpfSaveFileDialogService.cs b/src/dosymep.WpfCore/SimpleServices/WpfSaveFileDialogService.cs index c2ac462..282cb62 100644 --- a/src/dosymep.WpfCore/SimpleServices/WpfSaveFileDialogService.cs +++ b/src/dosymep.WpfCore/SimpleServices/WpfSaveFileDialogService.cs @@ -4,6 +4,8 @@ using Microsoft.Win32; +using SaveFileDialog = Microsoft.Win32.SaveFileDialog; + namespace dosymep.WpfCore.SimpleServices; /// diff --git a/src/dosymep.WpfCore/dosymep.WpfCore.csproj b/src/dosymep.WpfCore/dosymep.WpfCore.csproj index ada29dc..235f060 100644 --- a/src/dosymep.WpfCore/dosymep.WpfCore.csproj +++ b/src/dosymep.WpfCore/dosymep.WpfCore.csproj @@ -1,6 +1,7 @@  true + true enable 12 enable @@ -22,6 +23,7 @@ + diff --git a/src/dosymep.WpfUI.Core.Ninject/NinjectExtensions.cs b/src/dosymep.WpfUI.Core.Ninject/NinjectExtensions.cs index 7ff1f92..c87e209 100644 --- a/src/dosymep.WpfUI.Core.Ninject/NinjectExtensions.cs +++ b/src/dosymep.WpfUI.Core.Ninject/NinjectExtensions.cs @@ -1,5 +1,8 @@ +using System.Windows.Media; + using dosymep.SimpleServices; using dosymep.WpfCore.Behaviors; +using dosymep.WpfCore.SimpleServices; using dosymep.WpfUI.Core.SimpleServices; using Ninject; @@ -131,4 +134,90 @@ public static IKernel UseWpfUIProgressDialog(this IKernel kernel, return kernel; } + + /// + /// Добавляет в контейнер . + /// + /// Ninject контейнер. + /// Идентификатор приложения. + /// Имя автора по умолчанию. Расположение снизу справа на уведомлении. + /// Значение футера по умолчанию. Расположение снизу слева на уведомлении. + /// Значение изображения по умолчанию. Расположение слева на уведомлении. + /// Значение выбора окна. + /// По умолчанию . + /// Значение места отображения уведомления. + /// По умолчанию . + /// Максимальное количество уведомлений на экране. По умолчанию "5". + /// Возвращает настроенный контейнер Ninject. + /// kernel is null. + public static IKernel UseWpfUINotifications(this IKernel kernel, + string? applicationId = null, + string? defaultAuthor = null, + string? defaultFooter = null, + ImageSource? defaultImage = null, + NotificationScreen notificationScreen = NotificationScreen.Primary, + NotificationPosition notificationPosition = NotificationPosition.BottomRight, + int notificationVisibleMaxCount = 5) { + if(kernel == null) { + throw new ArgumentNullException(nameof(kernel)); + } + + kernel.Bind() + .To() + .WithPropertyValue(nameof(IAttachableService.AllowAttach), false) + .WithPropertyValue(nameof(WpfUINotificationService.ApplicationId), applicationId!) + .WithPropertyValue(nameof(WpfUINotificationService.DefaultAuthor), defaultAuthor!) + .WithPropertyValue(nameof(WpfUINotificationService.DefaultFooter), defaultFooter!) + .WithPropertyValue(nameof(WpfUINotificationService.DefaultImage), defaultImage!) + .WithPropertyValue(nameof(WpfUINotificationService.NotificationScreen), notificationScreen) + .WithPropertyValue(nameof(WpfUINotificationService.NotificationPosition), notificationPosition) + .WithPropertyValue(nameof(WpfUINotificationService.NotificationVisibleMaxCount), + notificationVisibleMaxCount); + + return kernel; + } + + /// + /// Добавляет в контейнер . + /// + /// Ninject контейнер. + /// Идентификатор приложения. + /// Имя автора по умолчанию. Расположение снизу справа на уведомлении. + /// Значение футера по умолчанию. Расположение снизу слева на уведомлении. + /// Значение изображения по умолчанию. Расположение слева на уведомлении. + /// Значение выбора окна. + /// По умолчанию . + /// Значение места отображения уведомления. + /// По умолчанию . + /// Максимальное количество уведомлений на экране. По умолчанию "5". + /// Тип ViewModel к которой будет прикрепление сервиса. + /// Возвращает настроенный контейнер Ninject. + /// Обязательно требуется прикрепить к элементу управления через . + /// kernel is null. + public static IKernel UseWpfUINotifications(this IKernel kernel, + string? applicationId = null, + string? defaultAuthor = null, + string? defaultFooter = null, + ImageSource? defaultImage = null, + NotificationScreen notificationScreen = NotificationScreen.ApplicationWindow, + NotificationPosition notificationPosition = NotificationPosition.BottomRight, + int notificationVisibleMaxCount = 5) { + if(kernel == null) { + throw new ArgumentNullException(nameof(kernel)); + } + + kernel.Bind() + .To() + .WhenInjectedInto() + .WithPropertyValue(nameof(IAttachableService.AllowAttach), true) + .WithPropertyValue(nameof(WpfUINotificationService.ApplicationId), applicationId!) + .WithPropertyValue(nameof(WpfUINotificationService.DefaultAuthor), defaultAuthor!) + .WithPropertyValue(nameof(WpfUINotificationService.DefaultFooter), defaultFooter!) + .WithPropertyValue(nameof(WpfUINotificationService.DefaultImage), defaultImage!) + .WithPropertyValue(nameof(WpfUINotificationService.NotificationScreen), notificationScreen) + .WithPropertyValue(nameof(WpfUINotificationService.NotificationPosition), notificationPosition) + .WithPropertyValue(nameof(WpfUINotificationService.NotificationVisibleMaxCount), notificationVisibleMaxCount); + + return kernel; + } } \ No newline at end of file diff --git a/src/dosymep.WpfUI.Core/SimpleServices/WpfUINotificationService.cs b/src/dosymep.WpfUI.Core/SimpleServices/WpfUINotificationService.cs new file mode 100644 index 0000000..e9b16d8 --- /dev/null +++ b/src/dosymep.WpfUI.Core/SimpleServices/WpfUINotificationService.cs @@ -0,0 +1,93 @@ +using System.Collections.ObjectModel; +using System.Windows; +using System.Windows.Media; + +using dosymep.SimpleServices; +using dosymep.WpfCore.SimpleServices; +using dosymep.WpfUI.Core.Windows; + +namespace dosymep.WpfUI.Core.SimpleServices; + +/// +/// Класс сервиса уведомлений +/// +public sealed class WpfUINotificationService : WpfBaseService, INotificationService { + private readonly IHasTheme _theme; + private readonly IHasLocalization _localization; + private readonly ObservableCollection _windowStack = []; + + /// + /// Создает экземпляр сервиса уведомлений. + /// + public WpfUINotificationService( + IHasTheme theme, + IHasLocalization localization) { + _theme = theme; + _localization = localization; + } + + /// + /// Идентификатор приложения. + /// + public string? ApplicationId { get; set; } + + /// + /// Значение автора по умолчанию. + /// + public string? DefaultAuthor { get; set; } + + /// + /// Значение футера по умолчанию. + /// + public string? DefaultFooter { get; set; } + + /// + /// Значение изображения по умолчанию. + /// + public ImageSource? DefaultImage { get; set; } + + /// + /// Определяет, на каком мониторе будет отображаться уведомление. + /// + public NotificationScreen NotificationScreen { get; set; } = NotificationScreen.Primary; + + /// + /// Позиция уведомления на экране. + /// + public NotificationPosition NotificationPosition { get; set; } = NotificationPosition.BottomRight; + + /// + /// Максимальное количество отображаемых уведомлений на экране. + /// + public int NotificationVisibleMaxCount { get; set; } = 1; + + /// + public INotification CreateNotification( + string title, string body, string? footer = null, string? author = null, ImageSource? imageSource = null) { + WpfUINotificationWindow window = new(_theme, _localization) { + Title = title, + Body = body, + Footer = footer ?? DefaultFooter, + Author = author ?? DefaultAuthor, + ImageSource = imageSource ?? DefaultImage, + WindowStack = _windowStack, + NotificationScreen = NotificationScreen, + NotificationPosition = NotificationPosition, + }; + + SetAssociatedOwner(window); + return window; + } + + /// + public INotification CreateFatalNotification( + string title, string body, string? author = null, ImageSource? imageSource = null) { + return CreateNotification("Ошибка", body, title, author, imageSource); + } + + /// + public INotification CreateWarningNotification( + string title, string body, string? author = null, ImageSource? imageSource = null) { + return CreateNotification("Предупреждение", body, title, author, imageSource); + } +} \ No newline at end of file diff --git a/src/dosymep.WpfUI.Core/Windows/WpfUINotificationWindow.xaml b/src/dosymep.WpfUI.Core/Windows/WpfUINotificationWindow.xaml new file mode 100644 index 0000000..6b89288 --- /dev/null +++ b/src/dosymep.WpfUI.Core/Windows/WpfUINotificationWindow.xaml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dosymep.WpfUI.Core/Windows/WpfUINotificationWindow.xaml.cs b/src/dosymep.WpfUI.Core/Windows/WpfUINotificationWindow.xaml.cs new file mode 100644 index 0000000..6d386d1 --- /dev/null +++ b/src/dosymep.WpfUI.Core/Windows/WpfUINotificationWindow.xaml.cs @@ -0,0 +1,232 @@ +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Diagnostics; +using System.Globalization; +using System.Windows; +using System.Windows.Interop; +using System.Windows.Media; +using System.Windows.Media.Animation; +using System.Windows.Threading; + +using dosymep.SimpleServices; +using dosymep.WpfCore.Behaviors; +using dosymep.WpfCore.SimpleServices; +using dosymep.WpfUI.Core.SimpleServices; + +using Wpf.Ui.Controls; + +namespace dosymep.WpfUI.Core.Windows; + +/// +/// Предоставляет окно уведомления. +/// +public partial class WpfUINotificationWindow : INotification { + private TaskCompletionSource? _tcs; + + /// + /// Определяет, на каком мониторе будет отображаться уведомление. + /// + public static readonly DependencyProperty NotificationScreenProperty = DependencyProperty.Register( + nameof(NotificationScreen), + typeof(NotificationScreen), + typeof(WpfUINotificationWindow), + new PropertyMetadata(default(NotificationScreen))); + + /// + /// Позиция уведомления на экране. + /// + public static readonly DependencyProperty NotificationPositionProperty = DependencyProperty.Register( + nameof(NotificationPosition), + typeof(NotificationPosition), + typeof(WpfUINotificationWindow), + new PropertyMetadata(default(NotificationPosition))); + + /// + /// Список открытых окон уведомлений. + /// + public static readonly DependencyProperty WindowStackProperty = DependencyProperty.Register( + nameof(WindowStack), + typeof(ObservableCollection), + typeof(WpfUINotificationWindow), + new PropertyMetadata(default(ObservableCollection))); + + /// + /// Идентифицирует свойство зависимости Body, + /// которое представляет основное содержимое или сообщение уведомления, отображаемого в уведомлении. + /// + public static readonly DependencyProperty BodyProperty = DependencyProperty.Register( + nameof(Body), + typeof(string), + typeof(WpfUINotificationWindow), + new PropertyMetadata(default(string))); + + /// + /// Идентифицирует свойство зависимости Footer, + /// которое представляет нижний текст или дополнительную информацию, отображаемую в уведомлении. + /// + public static readonly DependencyProperty FooterProperty = DependencyProperty.Register( + nameof(Footer), + typeof(string), + typeof(WpfUINotificationWindow), + new PropertyMetadata(default(string))); + + /// + /// Идентифицирует свойство зависимости Author, + /// которое представляет автора уведомления, отображаемого в уведомлении. + /// + public static readonly DependencyProperty AuthorProperty = DependencyProperty.Register( + nameof(Author), + typeof(string), + typeof(WpfUINotificationWindow), + new PropertyMetadata(default(string))); + + /// + /// Идентифицирует свойство зависимости ImageSource, + /// которое определяет источник изображения, отображаемого в уведомлении. + /// + public static readonly DependencyProperty ImageSourceProperty = DependencyProperty.Register( + nameof(ImageSource), + typeof(ImageSource), + typeof(WpfUINotificationWindow), + new PropertyMetadata(default(ImageSource))); + + private readonly IHasTheme _theme; + private readonly IHasLocalization _localization; + + /// + public event Action? ThemeChanged; + + /// + public event Action? LanguageChanged; + + /// + /// Предоставляет конструктор окна уведомления. + /// + public WpfUINotificationWindow( + IHasTheme theme, + IHasLocalization localization) { + _theme = theme; + _localization = localization; + + _theme.ThemeChanged += _ => ThemeChanged?.Invoke(_); + _localization.LanguageChanged += _ => LanguageChanged?.Invoke(_); + + theme.ThemeUpdaterService.SetTheme(theme.HostTheme, this); + + InitializeComponent(); + } + + /// + /// Определяет, на каком мониторе будет отображаться уведомление. + /// + public NotificationScreen NotificationScreen { + get => (NotificationScreen) GetValue(NotificationScreenProperty); + set => SetValue(NotificationScreenProperty, value); + } + + /// + /// Позиция уведомления на экране. + /// + public NotificationPosition NotificationPosition { + get => (NotificationPosition) GetValue(NotificationPositionProperty); + set => SetValue(NotificationPositionProperty, value); + } + + /// + /// Список открытых окон уведомлений. + /// + public ObservableCollection WindowStack { + get => (ObservableCollection) GetValue(WindowStackProperty); + set => SetValue(WindowStackProperty, value); + } + + /// + /// Представляет основное содержимое или сообщение, отображаемое в окне уведомления. + /// + public string? Body { + get => (string?) GetValue(BodyProperty); + set => SetValue(BodyProperty, value); + } + + /// + /// Идентифицирует свойство зависимости Footer, + /// которое представляет текст нижнего колонтитула, отображаемого в уведомлении. + /// + public string? Footer { + get => (string) GetValue(FooterProperty); + set => SetValue(FooterProperty, value); + } + + /// + /// Идентифицирует свойство зависимости Author, + /// которое представляет автора уведомления, отображаемого в окне уведомления. + /// + public string? Author { + get => (string?) GetValue(AuthorProperty); + set => SetValue(AuthorProperty, value); + } + + /// + /// Идентифицирует свойство зависимости ImageSource, + /// которое определяет источник изображения, отображаемого в уведомлении. + /// + public ImageSource? ImageSource { + get => (ImageSource?) GetValue(ImageSourceProperty); + set => SetValue(ImageSourceProperty, value); + } + + #region INotification + + Task INotification.ShowAsync() { + return ((INotification) this).ShowAsync(5000); + } + + Task INotification.ShowAsync(int millisecond) { + return ((INotification) this).ShowAsync(TimeSpan.FromMilliseconds(millisecond)); + } + + async Task INotification.ShowAsync(TimeSpan interval) { + _tcs = new TaskCompletionSource(); + + DispatcherTimer timer = new() {Interval = interval}; + timer.Start(); + + timer.Tick += (_, _) => { + if(!_tcs.Task.IsCompleted) { + _tcs?.SetResult(null); + _notificationWindowBehavior.OnAutoClosing(); + } + }; + + try { + Show(); + return await _tcs.Task; + } finally { + timer.Stop(); + } + } + + #endregion + + /// + protected override void OnExtendsContentIntoTitleBarChanged(bool oldValue, bool newValue) { + // do nothing + } + + /// + protected override void OnBackdropTypeChanged(WindowBackdropType oldValue, WindowBackdropType newValue) { + // do nothing + } + + /// + protected override void OnCornerPreferenceChanged(WindowCornerPreference oldValue, WindowCornerPreference newValue) { + // do nothing + } + + private void ButtonBase_OnClick(object sender, RoutedEventArgs e) { + if(_tcs?.Task.IsCompleted == false) { + _tcs?.SetResult(false); + _notificationWindowBehavior.OnClosing(); + } + } +} \ No newline at end of file