Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 8 additions & 29 deletions src/dosymep.WpfCore/Behaviors/WpfLocalizationBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,23 @@ public sealed class WpfLocalizationBehavior : Behavior<FrameworkElement> {
/// <inheritdoc />
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;
}

/// <inheritdoc />
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;
}
}

Expand Down
40 changes: 9 additions & 31 deletions src/dosymep.WpfCore/Behaviors/WpfThemeBehavior.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,44 +16,22 @@ public sealed class WpfThemeBehavior : Behavior<FrameworkElement> {
/// <inheritdoc />
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;
}

/// <inheritdoc />
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;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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
/// </summary>
internal sealed class LocalizationSourceExtensionConverter : TypeConverter {
/// <summary>
/// True if converting to an instance descriptor
/// </summary>
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) {
return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
}

/// <summary>
/// Converts to an instance descriptor
/// </summary>
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});
}
}
106 changes: 57 additions & 49 deletions src/dosymep.WpfCore/MarkupExtensions/EnumToItemsSourceExtension.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -15,11 +16,13 @@ namespace dosymep.WpfCore.MarkupExtensions;
/// <summary>
/// Конвертирует enum в список значений.
/// </summary>
[MarkupExtensionReturnType(typeof(string[]))]
[MarkupExtensionReturnType(typeof(MarkupValueObject))]
public sealed class EnumToItemsSourceExtension : MarkupExtension {
private readonly MarkupValueObject _markupValueObject = new();
private readonly Binding _binding = new(nameof(MarkupValueObject.Value));

private IHasLocalization? _localization;

/// <summary>
/// Конструирует объект.
/// </summary>
Expand All @@ -36,6 +39,8 @@ public EnumToItemsSourceExtension() { }
/// </summary>
public Type? EnumType { get; set; }

internal IReadOnlyCollection<EnumInfo>? ItemsSource => _markupValueObject.Value as IReadOnlyCollection<EnumInfo>;

/// <inheritdoc />
public override object? ProvideValue(IServiceProvider serviceProvider) {
if(EnumType is null) {
Expand All @@ -46,77 +51,80 @@ public EnumToItemsSourceExtension() { }
throw new InvalidOperationException("EnumType must be an enum.");
}

SetPropertyPaths(serviceProvider);
SetLocalizationStrings(serviceProvider);

return _binding.ProvideValue(serviceProvider);
}
// получаем корневой элемент,
// может быть Window, Page, UserControl
FrameworkElement? rootObject = serviceProvider.GetRootObject<FrameworkElement>();

private void SetLocalizationStrings(IServiceProvider serviceProvider) {
IHasLocalization? localization = serviceProvider.GetRootObject<IHasLocalization>();
ILocalizationService? localizationService = localization?.LocalizationService;
// для случая, если окно уже получено
_localization = rootObject as IHasLocalization;

if(localization is not null) {
localization.LanguageChanged += _ => UpdateDisplayName(_markupValueObject.Value);
if(_localization is null && rootObject is not null) {
// либо ждем его загрузку,
// чтобы можно было получить корневой элемент Window
rootObject.Loaded += RootObjectOnLoaded;
}

_binding.Source = _markupValueObject;
_markupValueObject.Value = GetEnumValues(localizationService);
}
_markupValueObject.Value = GetEnumValues();

private static void SetPropertyPaths(IServiceProvider serviceProvider) {
IProvideValueTarget? provideValueTarget = serviceProvider.GetService<IProvideValueTarget>();
if(provideValueTarget?.TargetObject is Selector selector) {
selector.SelectedValuePath = nameof(MarkupDisplayObject.Value);
}
// попытка установить значения
// отображаемого имени, контрол может быть уже загружен
TryUpdateDisplayName();

if(provideValueTarget?.TargetObject is ItemsControl itemsControl) {
itemsControl.DisplayMemberPath = nameof(MarkupDisplayObject.DisplayName);
}
// устанавливаем имена свойств обновляемого объекта
UpdateTargetControlProperties(serviceProvider);

_binding.Mode = BindingMode.OneWay;
_binding.FallbackValue = Array.Empty<EnumInfo>();
_binding.TargetNullValue = Array.Empty<EnumInfo>();

return _binding.ProvideValue(serviceProvider);
}

private void UpdateDisplayName(object? value) {
if(value == null) {
private void RootObjectOnLoaded(object sender, RoutedEventArgs e) {
if(sender is not FrameworkElement frameworkElement) {
return;
}

IEnumerable<MarkupDisplayObject> list = ((IEnumerable) value)
.OfType<MarkupDisplayObject>();
// отписываемся от события,
// потому что при смене темы может повторно вызваться
frameworkElement.Loaded -= RootObjectOnLoaded;

foreach(MarkupDisplayObject magicObject in list) {
magicObject.UpdateDisplayName();
_localization = Window.GetWindow(frameworkElement) as IHasLocalization;
if(_localization is not null) {
TryUpdateDisplayName();
_localization.LanguageChanged += _ => TryUpdateDisplayName();
}
}

private object[] GetEnumValues(ILocalizationService? localizationService) {
return EnumType?.GetFields(BindingFlags.Static | BindingFlags.Public)
.Select(item => CreateMarkupDisplayObject(item, localizationService))
.Cast<object>()
.ToArray() ?? Array.Empty<object>();
}
private void TryUpdateDisplayName() {
if(ItemsSource is null) {
return;
}

internal static MarkupDisplayObject CreateMarkupDisplayObject(FieldInfo item,
ILocalizationService? localizationService) {
return new MarkupDisplayObject(() => GetEnumName(item, localizationService)) {
Value = item.GetValue(null), DisplayName = GetEnumName(item, localizationService)
};
foreach(EnumInfo enumInfo in ItemsSource) {
enumInfo.UpdateDisplayName(_localization?.LocalizationService);
}
}

internal static string GetEnumName(FieldInfo item, ILocalizationService? localizationService) {
string? desciption = GetDescription(item);
private static void UpdateTargetControlProperties(IServiceProvider serviceProvider) {
IProvideValueTarget? provideValueTarget = serviceProvider.GetService<IProvideValueTarget>();
if(provideValueTarget?.TargetObject is Selector selector) {
selector.SelectedValuePath = nameof(EnumInfo.Id);
}

if(string.IsNullOrEmpty(desciption)) {
return localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}")
?? item.Name;
if(provideValueTarget?.TargetObject is ItemsControl itemsControl) {
itemsControl.DisplayMemberPath = nameof(EnumInfo.DisplayName);
}
}

return localizationService?.GetLocalizedString(desciption)
?? localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}")
?? desciption
?? item.Name;
private EnumInfo[] GetEnumValues() {
return EnumType?.GetFields(BindingFlags.Static | BindingFlags.Public)
.Select(CreateEnumInfo)
.ToArray() ?? [];
}

internal static string? GetDescription(FieldInfo fieldInfo) {
return fieldInfo.GetCustomAttribute<DescriptionAttribute>()?.Description;
private static EnumInfo CreateEnumInfo(FieldInfo fieldInfo) {
return new EnumInfo(fieldInfo.GetValue(null), fieldInfo);
}
}
Loading
Loading