From a874eaff6f0b321fd55069b130d6ffbf6749ff5b Mon Sep 17 00:00:00 2001 From: dosymep Date: Mon, 14 Jul 2025 16:50:37 +0300 Subject: [PATCH 1/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BD=D0=B2=D0=B5=D1=80=D1=82=D0=B5=D1=80=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../LocalizationSourceExtensionConverter.cs | 50 +++++++++++++++++++ .../LocalizationSourceExtension.cs | 12 ++--- 2 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 src/dosymep.WpfCore/Converters/LocalizationSourceExtensionConverter.cs diff --git a/src/dosymep.WpfCore/Converters/LocalizationSourceExtensionConverter.cs b/src/dosymep.WpfCore/Converters/LocalizationSourceExtensionConverter.cs new file mode 100644 index 0000000..e6f7224 --- /dev/null +++ b/src/dosymep.WpfCore/Converters/LocalizationSourceExtensionConverter.cs @@ -0,0 +1,50 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +// +// +// +// Contents: Implements a converter to an instance descriptor for +// DynamicResourceExtension +// +// + +using System.ComponentModel; +using System.ComponentModel.Design.Serialization; +using System.Windows; + +namespace dosymep.WpfCore.Converters; + +/// +/// Type converter to inform the serialization system how to construct a DynamicResourceExtension from +/// an instance. It reports that ResourceKey should be used as the first parameter to the constructor. +/// https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/DynamicResourceExtensionConverter.cs +/// +internal sealed class LocalizationSourceExtensionConverter : TypeConverter { + /// + /// True if converting to an instance descriptor + /// + public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) { + return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType); + } + + /// + /// Converts to an instance descriptor + /// + public override object? ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, + object value, Type destinationType) { + if(destinationType != typeof(InstanceDescriptor)) { + return base.ConvertTo(context, culture, value, destinationType); + } + + if(value == null) { + throw new ArgumentNullException(nameof(value)); + } + + if(value is not DynamicResourceExtension dynamicResource) + throw new ArgumentException("'value' must be of the type 'DynamicResourceExtension'", nameof(value)); + + return new InstanceDescriptor( + typeof(DynamicResourceExtension).GetConstructor([typeof(object)]), new[] {dynamicResource.ResourceKey}); + } +} \ No newline at end of file diff --git a/src/dosymep.WpfCore/MarkupExtensions/LocalizationSourceExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/LocalizationSourceExtension.cs index ed0a8e3..cb5f1cc 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/LocalizationSourceExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/LocalizationSourceExtension.cs @@ -1,12 +1,8 @@ using System.ComponentModel; -using System.Diagnostics; using System.Windows; -using System.Windows.Data; using System.Windows.Markup; -using System.Xaml; -using dosymep.SimpleServices; -using dosymep.WpfCore.MarkupExtensions.Internal; +using dosymep.WpfCore.Converters; namespace dosymep.WpfCore.MarkupExtensions; @@ -14,7 +10,7 @@ namespace dosymep.WpfCore.MarkupExtensions; /// Класс для получения локализации. /// [MarkupExtensionReturnType(typeof(object))] -[TypeConverter(typeof(DynamicResourceExtensionConverter))] +[TypeConverter(typeof(LocalizationSourceExtensionConverter))] public sealed class LocalizationSourceExtension : MarkupExtension { /// /// Конструирует объект. @@ -38,8 +34,8 @@ public LocalizationSourceExtension() { } throw new InvalidOperationException("ResourceKey is not set."); } - if(DesignerProperties.GetIsInDesignMode( - serviceProvider.GetRootObject()!)) { + DependencyObject? rootObject = serviceProvider.GetRootObject(); + if(rootObject == null || DesignerProperties.GetIsInDesignMode(rootObject)) { return ResourceKey; } From 70cf2b1e877f2d2bca6717494c00b9ce8d9417f0 Mon Sep 17 00:00:00 2001 From: dosymep Date: Mon, 14 Jul 2025 16:50:56 +0300 Subject: [PATCH 2/5] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BE=D1=88=D0=B8=D0=B1=D0=BA=D1=83=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=81=D0=B5=D1=80?= =?UTF-8?q?=D0=B2=D0=B8=D1=81=D0=BE=D0=B2=20=D0=B8=D0=B7=20=D1=80=D0=BE?= =?UTF-8?q?=D0=B4=D0=B8=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=BE=D0=BA=D0=BD=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EnumToItemsSourceExtension.cs | 45 +++++-- .../QualifiedImageExtension.cs | 116 ++++++++++-------- 2 files changed, 102 insertions(+), 59 deletions(-) diff --git a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs index 25a0e0a..9e317e4 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs @@ -1,6 +1,7 @@ using System.Collections; using System.ComponentModel; using System.Reflection; +using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; @@ -10,6 +11,8 @@ using dosymep.SimpleServices; using dosymep.WpfCore.MarkupExtensions.Internal; +using static System.Array; + namespace dosymep.WpfCore.MarkupExtensions; /// @@ -46,21 +49,41 @@ public EnumToItemsSourceExtension() { } throw new InvalidOperationException("EnumType must be an enum."); } + // устанавливаем имена свойств обновляемого объекта SetPropertyPaths(serviceProvider); - SetLocalizationStrings(serviceProvider); + + // получаем корневой элемент, + // может быть Window, Page, UserControl + FrameworkElement? rootObject = serviceProvider.GetRootObject(); + if(rootObject?.IsLoaded == true) { + // если элемент был загружен, устанавливаем значения + SetLocalizationStrings(serviceProvider.GetRootObject()); + } else { + if(rootObject != null) { + // либо ждем его загрузку, + // чтобы можно было получить корневой элемент Window + rootObject.Loaded += (s, e) => { + if(s is not DependencyObject dependencyObject) { + return; + } + + Window? rootWindow = Window.GetWindow(dependencyObject); + SetLocalizationStrings(rootWindow as IHasLocalization); + }; + } + } + _binding.Source = _markupValueObject; return _binding.ProvideValue(serviceProvider); } - private void SetLocalizationStrings(IServiceProvider serviceProvider) { - IHasLocalization? localization = serviceProvider.GetRootObject(); + private void SetLocalizationStrings(IHasLocalization? localization) { ILocalizationService? localizationService = localization?.LocalizationService; if(localization is not null) { localization.LanguageChanged += _ => UpdateDisplayName(_markupValueObject.Value); } - - _binding.Source = _markupValueObject; + _markupValueObject.Value = GetEnumValues(localizationService); } @@ -75,7 +98,7 @@ private static void SetPropertyPaths(IServiceProvider serviceProvider) { } } - private void UpdateDisplayName(object? value) { + private static void UpdateDisplayName(object? value) { if(value == null) { return; } @@ -92,7 +115,7 @@ private object[] GetEnumValues(ILocalizationService? localizationService) { return EnumType?.GetFields(BindingFlags.Static | BindingFlags.Public) .Select(item => CreateMarkupDisplayObject(item, localizationService)) .Cast() - .ToArray() ?? Array.Empty(); + .ToArray() ?? []; } internal static MarkupDisplayObject CreateMarkupDisplayObject(FieldInfo item, @@ -103,16 +126,16 @@ internal static MarkupDisplayObject CreateMarkupDisplayObject(FieldInfo item, } internal static string GetEnumName(FieldInfo item, ILocalizationService? localizationService) { - string? desciption = GetDescription(item); + string? description = GetDescription(item); - if(string.IsNullOrEmpty(desciption)) { + if(string.IsNullOrEmpty(description)) { return localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}") ?? item.Name; } - return localizationService?.GetLocalizedString(desciption) + return localizationService?.GetLocalizedString(description) ?? localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}") - ?? desciption + ?? description ?? item.Name; } diff --git a/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs index 09888b2..0e92761 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs @@ -2,6 +2,7 @@ using System.IO; using System.Reflection; using System.Resources; +using System.Windows; using System.Windows.Data; using System.Windows.Markup; using System.Windows.Media; @@ -48,33 +49,57 @@ public override object ProvideValue(IServiceProvider serviceProvider) { if(Uri == null) { throw new InvalidOperationException("Uri is not set."); } - - IHasTheme? theme = serviceProvider.GetRootObject(); - if(theme is not null) { - theme.ThemeChanged += _ => _markupValueObject.Value = GetImageUri(serviceProvider); - } - - IHasLocalization? localization = serviceProvider.GetRootObject(); - if(localization is not null) { - localization.LanguageChanged += _ => _markupValueObject.Value = GetImageUri(serviceProvider); - } - + + // получаем название библиотеки _cacheLibName = GetLibName(serviceProvider); _cacheBaseUri = new Uri($"pack://application:,,,/{_cacheLibName};component/"); _cacheResources ??= GetResources(_cacheLibName).ToHashSet(StringComparer.OrdinalIgnoreCase); - Uri item = GetImageUri(serviceProvider); + // получаем корневой элемент, + // может быть Window, Page, UserControl + FrameworkElement? rootObject = serviceProvider.GetRootObject(); + if(rootObject?.IsLoaded == true) { + // если элемент был загружен, устанавливаем значения + IHasTheme? theme = serviceProvider.GetRootObject(); + IHasLocalization? localization = serviceProvider.GetRootObject(); + + UpdateImage(theme, localization); + } else { + if(rootObject != null) { + // либо ждем его загрузку, + // чтобы можно было получить корневой элемент Window + rootObject.Loaded += (s, e) => { + if(s is not DependencyObject dependencyObject) { + return; + } + + Window? rootWindow = Window.GetWindow(dependencyObject); + UpdateImage(rootWindow as IHasTheme, rootWindow as IHasLocalization); + }; + } + } _binding.Source = _markupValueObject; - _markupValueObject.Value = item; return _binding.ProvideValue(serviceProvider); } - private Uri GetImageUri(IServiceProvider serviceProvider) { - HashSet variations = GetVariations(serviceProvider) + private void UpdateImage(IHasTheme? theme, IHasLocalization? localization) { + if(theme is not null) { + theme.ThemeChanged += _ => _markupValueObject.Value = GetImageUri(theme, localization); + } + + if(localization is not null) { + localization.LanguageChanged += _ => _markupValueObject.Value = GetImageUri(theme, localization); + } + + _markupValueObject.Value = GetImageUri(theme, localization); + } + + private Uri GetImageUri(IHasTheme? theme, IHasLocalization? localization) { + HashSet variations = GetVariations(theme, localization) .ToHashSet(StringComparer.OrdinalIgnoreCase); - + foreach(string resourceName in _cacheResources!) { if(variations!.Contains(resourceName)) { return new Uri(_cacheBaseUri!, resourceName); @@ -86,34 +111,6 @@ private Uri GetImageUri(IServiceProvider serviceProvider) { "pack://application:,,,/dosymep.Wpf.Core;component/assets/images/icons8-no-image-96.png", UriKind.Absolute); } - private List GetResources(string libName) { - List resources = new(); - - try { - Assembly? assembly = GetAssembly(libName); - using Stream? stream = assembly?.GetManifestResourceStream(libName + ".g.resources"); - - using ResourceReader resourceReader = new(stream!); - foreach(DictionaryEntry resource in resourceReader) { - resources.Add((string) resource.Key); - } - } catch { - // pass - } - - return resources; - } - - private Assembly? GetAssembly(string libName) { - try { - return Assembly.Load(libName); - } catch { - return AppDomain.CurrentDomain.GetAssemblies() - .Where(item => item.GetName().Name.Equals(_cacheLibName)) - .LastOrDefault(); - } - } - private string GetLibName(IServiceProvider serviceProvider) { IUriContext uriContext = (IUriContext) serviceProvider.GetService(typeof(IUriContext)); @@ -124,10 +121,7 @@ private string GetLibName(IServiceProvider serviceProvider) { return libName.Replace(";component", string.Empty); } - private IEnumerable GetVariations(IServiceProvider serviceProvider) { - IHasTheme? theme = serviceProvider.GetRootObject(); - IHasLocalization? localization = serviceProvider.GetRootObject(); - + private IEnumerable GetVariations(IHasTheme? theme, IHasLocalization? localization) { string? themeName = theme?.HostTheme.ToString(); string? cultureName = localization?.HostLanguage.Name; @@ -169,6 +163,32 @@ private IEnumerable GetVariations(IServiceProvider serviceProvider) { yield return string.Join("/", directoryName, fileName); } + private List GetResources(string libName) { + List resources = []; + + try { + Assembly? assembly = GetAssembly(libName); + using Stream? stream = assembly?.GetManifestResourceStream(libName + ".g.resources"); + + using ResourceReader resourceReader = new(stream!); + resources.AddRange(from DictionaryEntry resource in resourceReader select (string) resource.Key); + } catch { + // pass + } + + return resources; + } + + private Assembly? GetAssembly(string libName) { + try { + return Assembly.Load(libName); + } catch { + return AppDomain.CurrentDomain + .GetAssemblies() + .LastOrDefault(item => item.GetName().Name.Equals(_cacheLibName)); + } + } + private static IEnumerable Combinations(IEnumerable source) { if(source == null) { throw new ArgumentNullException(nameof(source)); From deb82968554468494a52ec5658d46e0d7a5cd1e6 Mon Sep 17 00:00:00 2001 From: dosymep Date: Tue, 15 Jul 2025 13:24:50 +0300 Subject: [PATCH 3/5] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BF=D0=B8=D1=81=D0=BA=D1=83=20=D0=BE=D1=82=20?= =?UTF-8?q?=D1=81=D0=BE=D0=B1=D1=8B=D1=82=D0=B8=D1=8F=20=D1=81=D1=80=D0=B0?= =?UTF-8?q?=D0=B7=D1=83=20=D0=B6=D0=B5=20=D0=BA=D0=B0=D0=BA=20=D0=BF=D1=80?= =?UTF-8?q?=D0=BE=D0=B9=D0=B4=D0=B5=D1=82=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EnumToItemsSourceExtension.cs | 24 ++++++++++++------- .../QualifiedImageExtension.cs | 23 +++++++++++------- 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs index 9e317e4..ad1535c 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs @@ -18,7 +18,7 @@ namespace dosymep.WpfCore.MarkupExtensions; /// /// Конвертирует enum в список значений. /// -[MarkupExtensionReturnType(typeof(string[]))] +[MarkupExtensionReturnType(typeof(MarkupValueObject))] public sealed class EnumToItemsSourceExtension : MarkupExtension { private readonly MarkupValueObject _markupValueObject = new(); private readonly Binding _binding = new(nameof(MarkupValueObject.Value)); @@ -62,14 +62,7 @@ public EnumToItemsSourceExtension() { } if(rootObject != null) { // либо ждем его загрузку, // чтобы можно было получить корневой элемент Window - rootObject.Loaded += (s, e) => { - if(s is not DependencyObject dependencyObject) { - return; - } - - Window? rootWindow = Window.GetWindow(dependencyObject); - SetLocalizationStrings(rootWindow as IHasLocalization); - }; + rootObject.Loaded += RootObjectOnLoaded; } } @@ -77,6 +70,19 @@ public EnumToItemsSourceExtension() { } return _binding.ProvideValue(serviceProvider); } + private void RootObjectOnLoaded(object sender, RoutedEventArgs e) { + if(sender is not FrameworkElement frameworkElement) { + return; + } + + // Отписываемся от события, + // потому что при смене темы может повторно вызваться + frameworkElement.Loaded -= RootObjectOnLoaded; + + Window? rootWindow = Window.GetWindow(frameworkElement); + SetLocalizationStrings(rootWindow as IHasLocalization); + } + private void SetLocalizationStrings(IHasLocalization? localization) { ILocalizationService? localizationService = localization?.LocalizationService; diff --git a/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs index 0e92761..910aafc 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/QualifiedImageExtension.cs @@ -68,22 +68,27 @@ public override object ProvideValue(IServiceProvider serviceProvider) { if(rootObject != null) { // либо ждем его загрузку, // чтобы можно было получить корневой элемент Window - rootObject.Loaded += (s, e) => { - if(s is not DependencyObject dependencyObject) { - return; - } - - Window? rootWindow = Window.GetWindow(dependencyObject); - UpdateImage(rootWindow as IHasTheme, rootWindow as IHasLocalization); - }; + rootObject.Loaded += RootObjectOnLoaded; } } _binding.Source = _markupValueObject; - return _binding.ProvideValue(serviceProvider); } + private void RootObjectOnLoaded(object sender, RoutedEventArgs e) { + if(sender is not FrameworkElement frameworkElement) { + return; + } + + // Отписываемся от события, + // потому что при смене темы может повторно вызваться + frameworkElement.Loaded -= RootObjectOnLoaded; + + Window? rootWindow = Window.GetWindow(frameworkElement); + UpdateImage(rootWindow as IHasTheme, rootWindow as IHasLocalization); + } + private void UpdateImage(IHasTheme? theme, IHasLocalization? localization) { if(theme is not null) { theme.ThemeChanged += _ => _markupValueObject.Value = GetImageUri(theme, localization); From 56231469ac8b5346889d6feb1976872bd9ec1fa2 Mon Sep 17 00:00:00 2001 From: dosymep Date: Tue, 15 Jul 2025 14:15:35 +0300 Subject: [PATCH 4/5] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=81=D0=BF=D0=BE=D1=81=D0=BE=D0=B1=20=D0=BF=D1=80=D0=B8?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=B5=D0=BC?= =?UTF-8?q?=D1=8B=20=D0=B8=20=D0=BB=D0=BE=D0=BA=D0=B0=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Behaviors/WpfLocalizationBehavior.cs | 37 ++++------------- .../Behaviors/WpfThemeBehavior.cs | 40 +++++-------------- 2 files changed, 17 insertions(+), 60 deletions(-) diff --git a/src/dosymep.WpfCore/Behaviors/WpfLocalizationBehavior.cs b/src/dosymep.WpfCore/Behaviors/WpfLocalizationBehavior.cs index 47f6180..ac0740f 100644 --- a/src/dosymep.WpfCore/Behaviors/WpfLocalizationBehavior.cs +++ b/src/dosymep.WpfCore/Behaviors/WpfLocalizationBehavior.cs @@ -17,44 +17,23 @@ public sealed class WpfLocalizationBehavior : Behavior { /// protected override void OnAttached() { _localization = AssociatedObject as IHasLocalization; - - // Если окно было уже загружено - // такое может быть, когда не используется behaviour в конструкторе - if(AssociatedObject.IsLoaded) { - Subscribe(); - } - - AssociatedObject.Loaded += AssociatedObjectOnLoaded; - AssociatedObject.Unloaded += AssociatedObjectOnUnloaded; + _localizationService = _localization?.LocalizationService; + AssociatedObject.Initialized += AssociatedObjectOnInitialized; } /// protected override void OnDetaching() { - Unsubscribe(); - AssociatedObject.Loaded -= AssociatedObjectOnLoaded; - AssociatedObject.Unloaded -= AssociatedObjectOnUnloaded; - } - - private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs e) { - Subscribe(); - } - - private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs e) { - Unsubscribe(); - } - - private void Subscribe() { if(_localization is not null) { - _localizationService = _localization.LocalizationService; - _localizationService?.SetLocalization(_localization.HostLanguage, AssociatedObject); - - _localization.LanguageChanged += LanguageOnThemeChanged; + _localization.LanguageChanged -= LanguageOnThemeChanged; } } - private void Unsubscribe() { + private void AssociatedObjectOnInitialized(object sender, EventArgs e) { + AssociatedObject.Initialized -= AssociatedObjectOnInitialized; + if(_localization is not null) { - _localization.LanguageChanged -= LanguageOnThemeChanged; + LanguageOnThemeChanged(_localization.HostLanguage); + _localization.LanguageChanged += LanguageOnThemeChanged; } } diff --git a/src/dosymep.WpfCore/Behaviors/WpfThemeBehavior.cs b/src/dosymep.WpfCore/Behaviors/WpfThemeBehavior.cs index 9381655..bff62b6 100644 --- a/src/dosymep.WpfCore/Behaviors/WpfThemeBehavior.cs +++ b/src/dosymep.WpfCore/Behaviors/WpfThemeBehavior.cs @@ -16,44 +16,22 @@ public sealed class WpfThemeBehavior : Behavior { /// protected override void OnAttached() { _theme = AssociatedObject as IHasTheme; - - // Если окно было уже загружено - // такое может быть, когда не используется behaviour в конструкторе - if(AssociatedObject.IsLoaded) { - Subscribe(); - } - - AssociatedObject.Loaded += AssociatedObjectOnLoaded; - AssociatedObject.Unloaded += AssociatedObjectOnUnloaded; + _themeUpdaterService = _theme?.ThemeUpdaterService; + AssociatedObject.Initialized += AssociatedObjectOnInitialized; } /// protected override void OnDetaching() { - Unsubscribe(); - AssociatedObject.Loaded -= AssociatedObjectOnLoaded; - AssociatedObject.Unloaded -= AssociatedObjectOnUnloaded; - } - - private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs e) { - Subscribe(); - } - - private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs e) { - Unsubscribe(); - } - - private void Subscribe() { - if(_theme != null) { - _themeUpdaterService = _theme.ThemeUpdaterService; - _themeUpdaterService?.SetTheme(_theme.HostTheme, AssociatedObject); - - _theme.ThemeChanged += ThemeOnThemeChanged; + if(_theme is not null) { + _theme.ThemeChanged -= ThemeOnThemeChanged; } } - private void Unsubscribe() { - if(_theme != null) { - _theme.ThemeChanged -= ThemeOnThemeChanged; + private void AssociatedObjectOnInitialized(object sender, EventArgs e) { + AssociatedObject.Initialized -= AssociatedObjectOnInitialized; + if(_theme is not null) { + ThemeOnThemeChanged(_theme.HostTheme); + _theme.ThemeChanged += ThemeOnThemeChanged; } } From 819fb2e205134e3197beca48ed403c77ba23d01d Mon Sep 17 00:00:00 2001 From: dosymep Date: Wed, 16 Jul 2025 11:23:18 +0300 Subject: [PATCH 5/5] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=B4,=20=D1=81=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB?= =?UTF-8?q?=20=D0=B8=D0=BD=D0=B8=D1=86=D0=B8=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8E=20=D0=BF=D0=BE=20=D0=B4=D1=80=D1=83=D0=B3?= =?UTF-8?q?=D0=BE=D0=BC=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../EnumToItemsSourceExtension.cs | 113 +++++++----------- .../MarkupExtensions/Internal/EnumInfo.cs | 90 ++++++++++++++ .../Internal/MarkupDisplayObject.cs | 31 ----- 3 files changed, 136 insertions(+), 98 deletions(-) create mode 100644 src/dosymep.WpfCore/MarkupExtensions/Internal/EnumInfo.cs delete mode 100644 src/dosymep.WpfCore/MarkupExtensions/Internal/MarkupDisplayObject.cs diff --git a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs index ad1535c..a9e214d 100644 --- a/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs +++ b/src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs @@ -11,8 +11,6 @@ using dosymep.SimpleServices; using dosymep.WpfCore.MarkupExtensions.Internal; -using static System.Array; - namespace dosymep.WpfCore.MarkupExtensions; /// @@ -23,6 +21,8 @@ public sealed class EnumToItemsSourceExtension : MarkupExtension { private readonly MarkupValueObject _markupValueObject = new(); private readonly Binding _binding = new(nameof(MarkupValueObject.Value)); + private IHasLocalization? _localization; + /// /// Конструирует объект. /// @@ -39,6 +39,8 @@ public EnumToItemsSourceExtension() { } /// public Type? EnumType { get; set; } + internal IReadOnlyCollection? ItemsSource => _markupValueObject.Value as IReadOnlyCollection; + /// public override object? ProvideValue(IServiceProvider serviceProvider) { if(EnumType is null) { @@ -49,24 +51,33 @@ public EnumToItemsSourceExtension() { } throw new InvalidOperationException("EnumType must be an enum."); } - // устанавливаем имена свойств обновляемого объекта - SetPropertyPaths(serviceProvider); - // получаем корневой элемент, // может быть Window, Page, UserControl FrameworkElement? rootObject = serviceProvider.GetRootObject(); - if(rootObject?.IsLoaded == true) { - // если элемент был загружен, устанавливаем значения - SetLocalizationStrings(serviceProvider.GetRootObject()); - } else { - if(rootObject != null) { - // либо ждем его загрузку, - // чтобы можно было получить корневой элемент Window - rootObject.Loaded += RootObjectOnLoaded; - } + + // для случая, если окно уже получено + _localization = rootObject as IHasLocalization; + + if(_localization is null && rootObject is not null) { + // либо ждем его загрузку, + // чтобы можно было получить корневой элемент Window + rootObject.Loaded += RootObjectOnLoaded; } _binding.Source = _markupValueObject; + _markupValueObject.Value = GetEnumValues(); + + // попытка установить значения + // отображаемого имени, контрол может быть уже загружен + TryUpdateDisplayName(); + + // устанавливаем имена свойств обновляемого объекта + UpdateTargetControlProperties(serviceProvider); + + _binding.Mode = BindingMode.OneWay; + _binding.FallbackValue = Array.Empty(); + _binding.TargetNullValue = Array.Empty(); + return _binding.ProvideValue(serviceProvider); } @@ -74,78 +85,46 @@ private void RootObjectOnLoaded(object sender, RoutedEventArgs e) { if(sender is not FrameworkElement frameworkElement) { return; } - - // Отписываемся от события, + + // отписываемся от события, // потому что при смене темы может повторно вызваться frameworkElement.Loaded -= RootObjectOnLoaded; - - Window? rootWindow = Window.GetWindow(frameworkElement); - SetLocalizationStrings(rootWindow as IHasLocalization); - } - - private void SetLocalizationStrings(IHasLocalization? localization) { - ILocalizationService? localizationService = localization?.LocalizationService; - if(localization is not null) { - localization.LanguageChanged += _ => UpdateDisplayName(_markupValueObject.Value); + _localization = Window.GetWindow(frameworkElement) as IHasLocalization; + if(_localization is not null) { + TryUpdateDisplayName(); + _localization.LanguageChanged += _ => TryUpdateDisplayName(); } - - _markupValueObject.Value = GetEnumValues(localizationService); } - private static void SetPropertyPaths(IServiceProvider serviceProvider) { - IProvideValueTarget? provideValueTarget = serviceProvider.GetService(); - if(provideValueTarget?.TargetObject is Selector selector) { - selector.SelectedValuePath = nameof(MarkupDisplayObject.Value); + private void TryUpdateDisplayName() { + if(ItemsSource is null) { + return; } - if(provideValueTarget?.TargetObject is ItemsControl itemsControl) { - itemsControl.DisplayMemberPath = nameof(MarkupDisplayObject.DisplayName); + foreach(EnumInfo enumInfo in ItemsSource) { + enumInfo.UpdateDisplayName(_localization?.LocalizationService); } } - private static void UpdateDisplayName(object? value) { - if(value == null) { - return; + private static void UpdateTargetControlProperties(IServiceProvider serviceProvider) { + IProvideValueTarget? provideValueTarget = serviceProvider.GetService(); + if(provideValueTarget?.TargetObject is Selector selector) { + selector.SelectedValuePath = nameof(EnumInfo.Id); } - IEnumerable list = ((IEnumerable) value) - .OfType(); - - foreach(MarkupDisplayObject magicObject in list) { - magicObject.UpdateDisplayName(); + if(provideValueTarget?.TargetObject is ItemsControl itemsControl) { + itemsControl.DisplayMemberPath = nameof(EnumInfo.DisplayName); } } - private object[] GetEnumValues(ILocalizationService? localizationService) { + private EnumInfo[] GetEnumValues() { return EnumType?.GetFields(BindingFlags.Static | BindingFlags.Public) - .Select(item => CreateMarkupDisplayObject(item, localizationService)) - .Cast() + .Select(CreateEnumInfo) .ToArray() ?? []; } - internal static MarkupDisplayObject CreateMarkupDisplayObject(FieldInfo item, - ILocalizationService? localizationService) { - return new MarkupDisplayObject(() => GetEnumName(item, localizationService)) { - Value = item.GetValue(null), DisplayName = GetEnumName(item, localizationService) - }; - } - - internal static string GetEnumName(FieldInfo item, ILocalizationService? localizationService) { - string? description = GetDescription(item); - - if(string.IsNullOrEmpty(description)) { - return localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}") - ?? item.Name; - } - - return localizationService?.GetLocalizedString(description) - ?? localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}") - ?? description - ?? item.Name; - } - - internal static string? GetDescription(FieldInfo fieldInfo) { - return fieldInfo.GetCustomAttribute()?.Description; + private static EnumInfo CreateEnumInfo(FieldInfo fieldInfo) { + return new EnumInfo(fieldInfo.GetValue(null), fieldInfo); } } \ No newline at end of file diff --git a/src/dosymep.WpfCore/MarkupExtensions/Internal/EnumInfo.cs b/src/dosymep.WpfCore/MarkupExtensions/Internal/EnumInfo.cs new file mode 100644 index 0000000..c67130f --- /dev/null +++ b/src/dosymep.WpfCore/MarkupExtensions/Internal/EnumInfo.cs @@ -0,0 +1,90 @@ +using System.ComponentModel; +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Windows; + +using dosymep.SimpleServices; + +namespace dosymep.WpfCore.MarkupExtensions.Internal; + +internal class EnumInfo : INotifyPropertyChanged, IEquatable { + private object? _displayName; + + public EnumInfo(object id, FieldInfo fieldInfo) { + Id = id; + FieldInfo = fieldInfo; + DisplayName = FieldInfo.Name; + } + + public object Id { get; } + public FieldInfo FieldInfo { get; } + + public object? DisplayName { + get => _displayName; + set => SetField(ref _displayName, value); + } + + public void UpdateDisplayName(ILocalizationService? localizationService) { + if(localizationService == null) { + DisplayName = FieldInfo.Name; + return; + } + + DisplayName = localizationService.GetLocalizedString( + $"{FieldInfo.FieldType.Name}.{FieldInfo.Name}") + ?? FieldInfo.Name; + } + + #region INotifyPropertyChanged + + public event PropertyChangedEventHandler? PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string? propertyName = null) { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + + protected bool SetField(ref T field, T value, [CallerMemberName] string? propertyName = null) { + if(EqualityComparer.Default.Equals(field, value)) return false; + field = value; + OnPropertyChanged(propertyName); + return true; + } + + #endregion + + #region IEquatable + + public bool Equals(EnumInfo? other) { + if(other is null) { + return false; + } + + return ReferenceEquals(this, other) || Id.Equals(other.Id); + } + + public override bool Equals(object? obj) { + if(obj is null) { + return false; + } + + if(ReferenceEquals(this, obj)) { + return true; + } + + return obj.GetType() == GetType() && Equals((EnumInfo) obj); + } + + public override int GetHashCode() { + return Id.GetHashCode(); + } + + public static bool operator ==(EnumInfo? left, EnumInfo? right) { + return Equals(left, right); + } + + public static bool operator !=(EnumInfo? left, EnumInfo? right) { + return !Equals(left, right); + } + + #endregion +} \ No newline at end of file diff --git a/src/dosymep.WpfCore/MarkupExtensions/Internal/MarkupDisplayObject.cs b/src/dosymep.WpfCore/MarkupExtensions/Internal/MarkupDisplayObject.cs deleted file mode 100644 index 3b7916d..0000000 --- a/src/dosymep.WpfCore/MarkupExtensions/Internal/MarkupDisplayObject.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Windows; - -namespace dosymep.WpfCore.MarkupExtensions.Internal; - -internal class MarkupDisplayObject : DependencyObject { - private readonly Func _updateDisplayName; - - public static readonly DependencyProperty ValueProperty = DependencyProperty.Register( - nameof(Value), typeof(object), typeof(MarkupDisplayObject), new PropertyMetadata(default(object))); - - public static readonly DependencyProperty DisplayNameProperty = DependencyProperty.Register( - nameof(DisplayName), typeof(object), typeof(MarkupDisplayObject), new PropertyMetadata(default(object))); - - public MarkupDisplayObject(Func updateDisplayName) { - _updateDisplayName = updateDisplayName; - } - - public object? Value { - get => GetValue(ValueProperty); - set => SetValue(ValueProperty, value); - } - - public object? DisplayName { - get => GetValue(DisplayNameProperty); - set => SetValue(DisplayNameProperty, value); - } - - public void UpdateDisplayName() { - DisplayName = _updateDisplayName(); - } -} \ No newline at end of file