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