diff --git a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs index dd7bd91bed..07ccd32c06 100644 --- a/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs +++ b/src/Avalonia.Base/Data/Core/UntypedBindingExpressionBase.cs @@ -282,7 +282,7 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, /// /// True if the value could be written to the binding source; otherwise false. /// - internal abstract bool WriteValueToSource(object? value); + internal virtual bool WriteValueToSource(object? value) => false; /// /// Converts a value using a value converter, logging a warning if necessary. @@ -368,6 +368,27 @@ public abstract class UntypedBindingExpressionBase : BindingExpressionBase, } } + /// + /// Logs a binding error. + /// + /// The error message. + /// The log level. + protected void Log(string error, LogEventLevel level = LogEventLevel.Warning) + { + if (!TryGetTarget(out var target)) + return; + + if (!Logger.TryGet(level, LogArea.Binding, out var log)) + return; + + log.Log( + target, + "An error occurred binding {Property} to {Expression}: {Message}", + (object?)TargetProperty ?? "(unknown)", + Description, + error); + } + /// /// Logs a binding error. /// diff --git a/src/Avalonia.Base/Styling/Setter.cs b/src/Avalonia.Base/Styling/Setter.cs index e454c64758..ec53892296 100644 --- a/src/Avalonia.Base/Styling/Setter.cs +++ b/src/Avalonia.Base/Styling/Setter.cs @@ -118,7 +118,7 @@ namespace Avalonia.Styling if (mode == BindingMode.OneWay || mode == BindingMode.TwoWay) { - if (i.Source is BindingExpression expression) + if (i.Expression is { } expression) { expression.Attach(target.GetValueStore(), target, Property, instance.Priority); return expression; diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 53cf6bc743..6ec9203f3f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -14,6 +14,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/DynamicResourceExpression.cs b/src/Markup/Avalonia.Markup.Xaml/Data/DynamicResourceExpression.cs new file mode 100644 index 0000000000..6b5eab1e94 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Data/DynamicResourceExpression.cs @@ -0,0 +1,149 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using Avalonia.Controls; +using Avalonia.Data.Core; +using Avalonia.Logging; +using Avalonia.Markup.Xaml.Converters; +using Avalonia.Media; +using Avalonia.Styling; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + internal class DynamicResourceExpression : UntypedBindingExpressionBase + { + private readonly object _resourceKey; + private readonly object? _anchor; + private IResourceHost? _host; + private IResourceProvider? _provider; + private bool _overrideThemeVariant; + private bool _targetTypeIsBrush; + private ThemeVariant? _themeVariant; + + public DynamicResourceExpression( + object resourceKey, + object? anchor, + ThemeVariant? themeVariant) + { + _resourceKey = resourceKey; + _anchor = anchor; + _themeVariant = themeVariant; + } + + public override string Description => $"DynamicResource {_resourceKey}"; + + protected override void StartCore() + { + if (!TryGetResourceHost(out _host)) + { + // The target is not an IResourceHost, so we need to find one from the anchor. + if (_anchor is IResourceProvider provider) + { + _provider = provider; + _host = provider.Owner; + _overrideThemeVariant = _themeVariant is not null; + } + } + + // If we wouldn't find a host or provider then log an error: we can't do anything. + if (_host is null && _provider is null) + { + Log("Unable to find IResourceHost or IResourceProvider from which to lookup " + + $"DynamicResource {_resourceKey}.", LogEventLevel.Error); + return; + } + + // Hook up events. + if (_provider is not null) + _provider.OwnerChanged += OnResourceProviderOwnerChanged; + Subscribe(_host); + + // And publish the initial value. + _targetTypeIsBrush = TargetType == typeof(IBrush); + PublishValue(); + } + + protected override void StopCore() + { + if (_provider is not null) + _provider.OwnerChanged -= OnResourceProviderOwnerChanged; + Unsubscribe(_host); + _host = null; + _provider = null; + } + + private void OnResourceProviderOwnerChanged(object? sender, EventArgs e) + { + Unsubscribe(_host); + _host = _provider?.Owner; + Subscribe(_host); + PublishValue(); + } + + private void ResourcesChanged(object? sender, ResourcesChangedEventArgs e) => PublishValue(); + + private void ActualThemeVariantChanged(object? sender, EventArgs e) + { + _themeVariant = ((IThemeVariantHost)sender!).ActualThemeVariant; + PublishValue(); + } + + private void PublishValue() + { + if (_host is not null) + { + var theme = _themeVariant; + var value = _host.FindResource(theme, _resourceKey) ?? AvaloniaProperty.UnsetValue; + var convertedValue = _targetTypeIsBrush ? + ColorToBrushConverter.Convert(value, typeof(IBrush)) : + value; + PublishValue(convertedValue); + } + else + { + PublishValue(AvaloniaProperty.UnsetValue); + } + } + + private bool TryGetResourceHost([NotNullWhen(true)] out IResourceHost? host) + { + if (TryGetTarget(out var target) && target is IResourceHost targetHost) + { + host = targetHost; + return true; + } + + if (_anchor is IResourceHost anchorHost) + { + host = anchorHost; + return host is not null; + } + + host = null; + return false; + } + + private void Subscribe(IResourceHost? host) + { + if (host is not null) + { + host.ResourcesChanged += ResourcesChanged; + + if (!_overrideThemeVariant && _host is IThemeVariantHost themeVariantHost) + { + _themeVariant = themeVariantHost.ActualThemeVariant; + themeVariantHost.ActualThemeVariantChanged += ActualThemeVariantChanged; + } + } + } + + private void Unsubscribe(IResourceHost? host) + { + if (host is not null) + { + host.ResourcesChanged -= ResourcesChanged; + if (!_overrideThemeVariant && _host is IThemeVariantHost themeVariantHost) + themeVariantHost.ActualThemeVariantChanged -= ActualThemeVariantChanged; + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 7f52c872ed..cbce88fbea 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -1,6 +1,9 @@ using System; +using System.Diagnostics.CodeAnalysis; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Data.Core; +using Avalonia.Logging; using Avalonia.Markup.Xaml.Converters; using Avalonia.Media; using Avalonia.Styling; @@ -11,7 +14,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions { private object? _anchor; private BindingPriority _priority; - private ThemeVariant? _currentThemeVariant; + private ThemeVariant? _themeVariant; public DynamicResourceExtension() { @@ -38,7 +41,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions (object?)serviceProvider.GetFirstParent(); } - _currentThemeVariant = StaticResourceExtension.GetDictionaryVariant(serviceProvider); + _themeVariant = StaticResourceExtension.GetDictionaryVariant(serviceProvider); return this; } @@ -50,34 +53,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions bool enableDataValidation) { if (ResourceKey is null) - { return null; - } - - var control = target as IResourceHost ?? _anchor as IResourceHost; - - if (control != null) - { - var source = control.GetResourceObservable(ResourceKey, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source, _priority); - } - else if (_anchor is IResourceProvider resourceProvider) - { - var source = resourceProvider.GetResourceObservable(ResourceKey, _currentThemeVariant, GetConverter(targetProperty)); - return InstancedBinding.OneWay(source, _priority); - } - - return null; - } - - private static Func? GetConverter(AvaloniaProperty? targetProperty) - { - if (targetProperty?.PropertyType == typeof(IBrush)) - { - return x => ColorToBrushConverter.Convert(x, typeof(IBrush)); - } - - return null; + var expression = new DynamicResourceExpression(ResourceKey, _anchor, _themeVariant); + return new InstancedBinding(target, expression, BindingMode.OneWay, _priority); } } }