Skip to content

Commit aa67161

Browse files
authored
Исправление применения локализации (#8)
1 parent 8ac4415 commit aa67161

File tree

8 files changed

+292
-197
lines changed

8 files changed

+292
-197
lines changed

src/dosymep.WpfCore/Behaviors/WpfLocalizationBehavior.cs

Lines changed: 8 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,44 +17,23 @@ public sealed class WpfLocalizationBehavior : Behavior<FrameworkElement> {
1717
/// <inheritdoc />
1818
protected override void OnAttached() {
1919
_localization = AssociatedObject as IHasLocalization;
20-
21-
// Если окно было уже загружено
22-
// такое может быть, когда не используется behaviour в конструкторе
23-
if(AssociatedObject.IsLoaded) {
24-
Subscribe();
25-
}
26-
27-
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
28-
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
20+
_localizationService = _localization?.LocalizationService;
21+
AssociatedObject.Initialized += AssociatedObjectOnInitialized;
2922
}
3023

3124
/// <inheritdoc />
3225
protected override void OnDetaching() {
33-
Unsubscribe();
34-
AssociatedObject.Loaded -= AssociatedObjectOnLoaded;
35-
AssociatedObject.Unloaded -= AssociatedObjectOnUnloaded;
36-
}
37-
38-
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs e) {
39-
Subscribe();
40-
}
41-
42-
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs e) {
43-
Unsubscribe();
44-
}
45-
46-
private void Subscribe() {
4726
if(_localization is not null) {
48-
_localizationService = _localization.LocalizationService;
49-
_localizationService?.SetLocalization(_localization.HostLanguage, AssociatedObject);
50-
51-
_localization.LanguageChanged += LanguageOnThemeChanged;
27+
_localization.LanguageChanged -= LanguageOnThemeChanged;
5228
}
5329
}
5430

55-
private void Unsubscribe() {
31+
private void AssociatedObjectOnInitialized(object sender, EventArgs e) {
32+
AssociatedObject.Initialized -= AssociatedObjectOnInitialized;
33+
5634
if(_localization is not null) {
57-
_localization.LanguageChanged -= LanguageOnThemeChanged;
35+
LanguageOnThemeChanged(_localization.HostLanguage);
36+
_localization.LanguageChanged += LanguageOnThemeChanged;
5837
}
5938
}
6039

src/dosymep.WpfCore/Behaviors/WpfThemeBehavior.cs

Lines changed: 9 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,22 @@ public sealed class WpfThemeBehavior : Behavior<FrameworkElement> {
1616
/// <inheritdoc />
1717
protected override void OnAttached() {
1818
_theme = AssociatedObject as IHasTheme;
19-
20-
// Если окно было уже загружено
21-
// такое может быть, когда не используется behaviour в конструкторе
22-
if(AssociatedObject.IsLoaded) {
23-
Subscribe();
24-
}
25-
26-
AssociatedObject.Loaded += AssociatedObjectOnLoaded;
27-
AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
19+
_themeUpdaterService = _theme?.ThemeUpdaterService;
20+
AssociatedObject.Initialized += AssociatedObjectOnInitialized;
2821
}
2922

3023
/// <inheritdoc />
3124
protected override void OnDetaching() {
32-
Unsubscribe();
33-
AssociatedObject.Loaded -= AssociatedObjectOnLoaded;
34-
AssociatedObject.Unloaded -= AssociatedObjectOnUnloaded;
35-
}
36-
37-
private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs e) {
38-
Subscribe();
39-
}
40-
41-
private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs e) {
42-
Unsubscribe();
43-
}
44-
45-
private void Subscribe() {
46-
if(_theme != null) {
47-
_themeUpdaterService = _theme.ThemeUpdaterService;
48-
_themeUpdaterService?.SetTheme(_theme.HostTheme, AssociatedObject);
49-
50-
_theme.ThemeChanged += ThemeOnThemeChanged;
25+
if(_theme is not null) {
26+
_theme.ThemeChanged -= ThemeOnThemeChanged;
5127
}
5228
}
5329

54-
private void Unsubscribe() {
55-
if(_theme != null) {
56-
_theme.ThemeChanged -= ThemeOnThemeChanged;
30+
private void AssociatedObjectOnInitialized(object sender, EventArgs e) {
31+
AssociatedObject.Initialized -= AssociatedObjectOnInitialized;
32+
if(_theme is not null) {
33+
ThemeOnThemeChanged(_theme.HostTheme);
34+
_theme.ThemeChanged += ThemeOnThemeChanged;
5735
}
5836
}
5937

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
//
5+
//
6+
//
7+
// Contents: Implements a converter to an instance descriptor for
8+
// DynamicResourceExtension
9+
//
10+
//
11+
12+
using System.ComponentModel;
13+
using System.ComponentModel.Design.Serialization;
14+
using System.Windows;
15+
16+
namespace dosymep.WpfCore.Converters;
17+
18+
/// <summary>
19+
/// Type converter to inform the serialization system how to construct a DynamicResourceExtension from
20+
/// an instance. It reports that ResourceKey should be used as the first parameter to the constructor.
21+
/// https://github.com/dotnet/wpf/blob/main/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/DynamicResourceExtensionConverter.cs
22+
/// </summary>
23+
internal sealed class LocalizationSourceExtensionConverter : TypeConverter {
24+
/// <summary>
25+
/// True if converting to an instance descriptor
26+
/// </summary>
27+
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType) {
28+
return destinationType == typeof(InstanceDescriptor) || base.CanConvertTo(context, destinationType);
29+
}
30+
31+
/// <summary>
32+
/// Converts to an instance descriptor
33+
/// </summary>
34+
public override object? ConvertTo(ITypeDescriptorContext context, System.Globalization.CultureInfo culture,
35+
object value, Type destinationType) {
36+
if(destinationType != typeof(InstanceDescriptor)) {
37+
return base.ConvertTo(context, culture, value, destinationType);
38+
}
39+
40+
if(value == null) {
41+
throw new ArgumentNullException(nameof(value));
42+
}
43+
44+
if(value is not DynamicResourceExtension dynamicResource)
45+
throw new ArgumentException("'value' must be of the type 'DynamicResourceExtension'", nameof(value));
46+
47+
return new InstanceDescriptor(
48+
typeof(DynamicResourceExtension).GetConstructor([typeof(object)]), new[] {dynamicResource.ResourceKey});
49+
}
50+
}
Lines changed: 57 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Collections;
22
using System.ComponentModel;
33
using System.Reflection;
4+
using System.Windows;
45
using System.Windows.Controls;
56
using System.Windows.Controls.Primitives;
67
using System.Windows.Data;
@@ -15,11 +16,13 @@ namespace dosymep.WpfCore.MarkupExtensions;
1516
/// <summary>
1617
/// Конвертирует enum в список значений.
1718
/// </summary>
18-
[MarkupExtensionReturnType(typeof(string[]))]
19+
[MarkupExtensionReturnType(typeof(MarkupValueObject))]
1920
public sealed class EnumToItemsSourceExtension : MarkupExtension {
2021
private readonly MarkupValueObject _markupValueObject = new();
2122
private readonly Binding _binding = new(nameof(MarkupValueObject.Value));
2223

24+
private IHasLocalization? _localization;
25+
2326
/// <summary>
2427
/// Конструирует объект.
2528
/// </summary>
@@ -36,6 +39,8 @@ public EnumToItemsSourceExtension() { }
3639
/// </summary>
3740
public Type? EnumType { get; set; }
3841

42+
internal IReadOnlyCollection<EnumInfo>? ItemsSource => _markupValueObject.Value as IReadOnlyCollection<EnumInfo>;
43+
3944
/// <inheritdoc />
4045
public override object? ProvideValue(IServiceProvider serviceProvider) {
4146
if(EnumType is null) {
@@ -46,77 +51,80 @@ public EnumToItemsSourceExtension() { }
4651
throw new InvalidOperationException("EnumType must be an enum.");
4752
}
4853

49-
SetPropertyPaths(serviceProvider);
50-
SetLocalizationStrings(serviceProvider);
51-
52-
return _binding.ProvideValue(serviceProvider);
53-
}
54+
// получаем корневой элемент,
55+
// может быть Window, Page, UserControl
56+
FrameworkElement? rootObject = serviceProvider.GetRootObject<FrameworkElement>();
5457

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

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

6367
_binding.Source = _markupValueObject;
64-
_markupValueObject.Value = GetEnumValues(localizationService);
65-
}
68+
_markupValueObject.Value = GetEnumValues();
6669

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

73-
if(provideValueTarget?.TargetObject is ItemsControl itemsControl) {
74-
itemsControl.DisplayMemberPath = nameof(MarkupDisplayObject.DisplayName);
75-
}
74+
// устанавливаем имена свойств обновляемого объекта
75+
UpdateTargetControlProperties(serviceProvider);
76+
77+
_binding.Mode = BindingMode.OneWay;
78+
_binding.FallbackValue = Array.Empty<EnumInfo>();
79+
_binding.TargetNullValue = Array.Empty<EnumInfo>();
80+
81+
return _binding.ProvideValue(serviceProvider);
7682
}
7783

78-
private void UpdateDisplayName(object? value) {
79-
if(value == null) {
84+
private void RootObjectOnLoaded(object sender, RoutedEventArgs e) {
85+
if(sender is not FrameworkElement frameworkElement) {
8086
return;
8187
}
8288

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

86-
foreach(MarkupDisplayObject magicObject in list) {
87-
magicObject.UpdateDisplayName();
93+
_localization = Window.GetWindow(frameworkElement) as IHasLocalization;
94+
if(_localization is not null) {
95+
TryUpdateDisplayName();
96+
_localization.LanguageChanged += _ => TryUpdateDisplayName();
8897
}
8998
}
9099

91-
private object[] GetEnumValues(ILocalizationService? localizationService) {
92-
return EnumType?.GetFields(BindingFlags.Static | BindingFlags.Public)
93-
.Select(item => CreateMarkupDisplayObject(item, localizationService))
94-
.Cast<object>()
95-
.ToArray() ?? Array.Empty<object>();
96-
}
100+
private void TryUpdateDisplayName() {
101+
if(ItemsSource is null) {
102+
return;
103+
}
97104

98-
internal static MarkupDisplayObject CreateMarkupDisplayObject(FieldInfo item,
99-
ILocalizationService? localizationService) {
100-
return new MarkupDisplayObject(() => GetEnumName(item, localizationService)) {
101-
Value = item.GetValue(null), DisplayName = GetEnumName(item, localizationService)
102-
};
105+
foreach(EnumInfo enumInfo in ItemsSource) {
106+
enumInfo.UpdateDisplayName(_localization?.LocalizationService);
107+
}
103108
}
104109

105-
internal static string GetEnumName(FieldInfo item, ILocalizationService? localizationService) {
106-
string? desciption = GetDescription(item);
110+
private static void UpdateTargetControlProperties(IServiceProvider serviceProvider) {
111+
IProvideValueTarget? provideValueTarget = serviceProvider.GetService<IProvideValueTarget>();
112+
if(provideValueTarget?.TargetObject is Selector selector) {
113+
selector.SelectedValuePath = nameof(EnumInfo.Id);
114+
}
107115

108-
if(string.IsNullOrEmpty(desciption)) {
109-
return localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}")
110-
?? item.Name;
116+
if(provideValueTarget?.TargetObject is ItemsControl itemsControl) {
117+
itemsControl.DisplayMemberPath = nameof(EnumInfo.DisplayName);
111118
}
119+
}
112120

113-
return localizationService?.GetLocalizedString(desciption)
114-
?? localizationService?.GetLocalizedString($"{item.FieldType.Name}.{item.Name}")
115-
?? desciption
116-
?? item.Name;
121+
private EnumInfo[] GetEnumValues() {
122+
return EnumType?.GetFields(BindingFlags.Static | BindingFlags.Public)
123+
.Select(CreateEnumInfo)
124+
.ToArray() ?? [];
117125
}
118126

119-
internal static string? GetDescription(FieldInfo fieldInfo) {
120-
return fieldInfo.GetCustomAttribute<DescriptionAttribute>()?.Description;
127+
private static EnumInfo CreateEnumInfo(FieldInfo fieldInfo) {
128+
return new EnumInfo(fieldInfo.GetValue(null), fieldInfo);
121129
}
122130
}

0 commit comments

Comments
 (0)