Browse Source

Merge pull request #3597 from AvaloniaUI/fixes/3590-nested-resources

Fix DynamicResource in Style resources
pull/3637/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
0a579a2fa9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 37
      src/Avalonia.Controls/Application.cs
  2. 62
      src/Avalonia.Styling/StyledElement.cs
  3. 46
      src/Avalonia.Styling/Styling/Styles.cs
  4. 131
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  5. 85
      tests/Avalonia.Styling.UnitTests/StyledElementTests.cs
  6. 19
      tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs
  7. 26
      tests/Avalonia.Styling.UnitTests/StylesTests.cs

37
src/Avalonia.Controls/Application.cs

@ -44,6 +44,7 @@ namespace Avalonia
private readonly Styler _styler = new Styler();
private Styles _styles;
private IResourceDictionary _resources;
private bool _notifyingResourcesChanged;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
@ -160,7 +161,19 @@ namespace Avalonia
/// <remarks>
/// Global styles apply to all windows in the application.
/// </remarks>
public Styles Styles => _styles ?? (_styles = new Styles());
public Styles Styles
{
get
{
if (_styles == null)
{
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
/// <inheritdoc/>
bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null;
@ -233,9 +246,29 @@ namespace Avalonia
}
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
if (_notifyingResourcesChanged)
{
return;
}
try
{
_notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
finally
{
_notifyingResourcesChanged = false;
}
}
private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
NotifyResourcesChanged(e);
}
private string _name;

62
src/Avalonia.Styling/StyledElement.cs

@ -67,6 +67,7 @@ namespace Avalonia
private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
private ITemplatedControl _templatedParent;
private bool _dataContextUpdating;
private bool _notifyingResourcesChanged;
/// <summary>
/// Initializes static members of the <see cref="StyledElement"/> class.
@ -214,28 +215,15 @@ namespace Avalonia
/// </remarks>
public Styles Styles
{
get { return _styles ?? (Styles = new Styles()); }
set
get
{
Contract.Requires<ArgumentNullException>(value != null);
if (_styles != value)
if (_styles == null)
{
if (_styles != null)
{
(_styles as ISetResourceParent)?.SetParent(null);
_styles.ResourcesChanged -= ThisResourcesChanged;
}
_styles = value;
if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
{
setParent.SetParent(this);
}
_styles = new Styles(this);
_styles.ResourcesChanged += ThisResourcesChanged;
}
return _styles;
}
}
@ -253,6 +241,7 @@ namespace Avalonia
if (_resources != null)
{
(_resources as ISetResourceParent)?.SetParent(null);
hadResources = _resources.Count > 0;
_resources.ResourcesChanged -= ThisResourcesChanged;
}
@ -260,9 +249,14 @@ namespace Avalonia
_resources = value;
_resources.ResourcesChanged += ThisResourcesChanged;
if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
{
setParent.SetParent(this);
}
if (hadResources || _resources.Count > 0)
{
((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
NotifyResourcesChanged(new ResourcesChangedEventArgs());
}
}
}
@ -407,10 +401,7 @@ namespace Avalonia
}
/// <inheritdoc/>
void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) => NotifyResourcesChanged(e);
/// <inheritdoc/>
bool IResourceProvider.TryGetResource(object key, out object value)
@ -456,7 +447,8 @@ namespace Avalonia
{
Parent.ResourcesChanged += ThisResourcesChanged;
}
((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
NotifyResourcesChanged(new ResourcesChangedEventArgs());
if (Parent is ILogicalRoot || Parent?.IsAttachedToLogicalTree == true || this is ILogicalRoot)
{
@ -721,9 +713,29 @@ namespace Avalonia
}
}
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
if (_notifyingResourcesChanged)
{
return;
}
try
{
_notifyingResourcesChanged = true;
(_resources as ISetResourceParent)?.ParentResourcesChanged(e);
(_styles as ISetResourceParent)?.ParentResourcesChanged(e);
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
finally
{
_notifyingResourcesChanged = false;
}
}
private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
((ILogical)this).NotifyResourcesChanged(e);
NotifyResourcesChanged(e);
}
}
}

46
src/Avalonia.Styling/Styling/Styles.cs

@ -20,6 +20,7 @@ namespace Avalonia.Styling
private IResourceDictionary _resources;
private AvaloniaList<IStyle> _styles = new AvaloniaList<IStyle>();
private Dictionary<Type, List<IStyle>> _cache;
private bool _notifyingResourcesChanged;
public Styles()
{
@ -38,7 +39,7 @@ namespace Avalonia.Styling
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
x.ResourcesChanged += SubResourceChanged;
x.ResourcesChanged += NotifyResourcesChanged;
_cache = null;
},
x =>
@ -54,12 +55,18 @@ namespace Avalonia.Styling
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
x.ResourcesChanged -= SubResourceChanged;
x.ResourcesChanged -= NotifyResourcesChanged;
_cache = null;
},
() => { });
}
public Styles(IResourceNode parent)
: this()
{
_parent = parent;
}
public event NotifyCollectionChangedEventHandler CollectionChanged
{
add => _styles.CollectionChanged += value;
@ -90,11 +97,11 @@ namespace Avalonia.Styling
if (_resources != null)
{
hadResources = _resources.Count > 0;
_resources.ResourcesChanged -= ResourceDictionaryChanged;
_resources.ResourcesChanged -= NotifyResourcesChanged;
}
_resources = value;
_resources.ResourcesChanged += ResourceDictionaryChanged;
_resources.ResourcesChanged += NotifyResourcesChanged;
if (hadResources || _resources.Count > 0)
{
@ -261,34 +268,35 @@ namespace Avalonia.Styling
/// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
NotifyResourcesChanged(e);
}
private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e)
private void NotifyResourcesChanged(object sender, ResourcesChangedEventArgs e)
{
foreach (var child in this)
{
(child as ISetResourceParent)?.ParentResourcesChanged(e);
}
ResourcesChanged?.Invoke(this, e);
NotifyResourcesChanged(e);
}
private void SubResourceChanged(object sender, ResourcesChangedEventArgs e)
private void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
var foundSource = false;
if (_notifyingResourcesChanged)
{
return;
}
foreach (var child in this)
try
{
if (foundSource)
_notifyingResourcesChanged = true;
foreach (var child in this)
{
(child as ISetResourceParent)?.ParentResourcesChanged(e);
}
foundSource |= child == sender;
ResourcesChanged?.Invoke(this, e);
}
finally
{
_notifyingResourcesChanged = false;
}
ResourcesChanged?.Invoke(this, e);
}
}
}

131
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@ -627,6 +627,137 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(0xff506070, brush.Color.ToUint32());
}
[Fact]
public void Resource_With_DynamicResource_Is_Updated_When_Added_To_Parent()
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<SolidColorBrush x:Key='brush' Color='{DynamicResource color}'/>
</UserControl.Resources>
<Border Name='border' Background='{DynamicResource brush}'/>
</UserControl>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var border = userControl.FindControl<Border>("border");
DelayedBinding.ApplyBindings(border);
var brush = (SolidColorBrush)border.Background;
Assert.Equal(0u, brush.Color.ToUint32());
brush.GetObservable(SolidColorBrush.ColorProperty).Subscribe(_ => { });
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window
{
Resources =
{
{ "color", Colors.Red }
},
Content = userControl,
};
window.Show();
Assert.Equal(Colors.Red, brush.Color);
}
}
[Fact]
public void MergedDictionary_Resource_With_DynamicResource_Is_Updated_When_Added_To_Parent()
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<SolidColorBrush x:Key='brush' Color='{DynamicResource color}'/>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Border Name='border' Background='{DynamicResource brush}'/>
</UserControl>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var border = userControl.FindControl<Border>("border");
DelayedBinding.ApplyBindings(border);
var brush = (SolidColorBrush)border.Background;
Assert.Equal(0u, brush.Color.ToUint32());
brush.GetObservable(SolidColorBrush.ColorProperty).Subscribe(_ => { });
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window
{
Resources =
{
{ "color", Colors.Red }
},
Content = userControl,
};
window.Show();
Assert.Equal(Colors.Red, brush.Color);
}
}
[Fact]
public void Style_Resource_With_DynamicResource_Is_Updated_When_Added_To_Parent()
{
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Styles>
<Style>
<Style.Resources>
<SolidColorBrush x:Key='brush' Color='{DynamicResource color}'/>
</Style.Resources>
</Style>
</UserControl.Styles>
<Border Name='border' Background='{DynamicResource brush}'/>
</UserControl>";
var loader = new AvaloniaXamlLoader();
var userControl = (UserControl)loader.Load(xaml);
var border = userControl.FindControl<Border>("border");
DelayedBinding.ApplyBindings(border);
var brush = (SolidColorBrush)border.Background;
Assert.Equal(0u, brush.Color.ToUint32());
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window
{
Resources =
{
{ "color", Colors.Red }
},
Content = userControl,
};
window.Show();
Assert.Equal(Colors.Red, brush.Color);
}
}
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(

85
tests/Avalonia.Styling.UnitTests/StyledElementTests.cs

@ -489,6 +489,91 @@ namespace Avalonia.Styling.UnitTests
called);
}
[Fact]
public void Resources_Parent_Is_Set()
{
var target = new TestControl();
Assert.Same(target, ((IResourceNode)target.Resources).ResourceParent);
}
[Fact]
public void Assigned_Resources_Parent_Is_Set()
{
var resources = new ResourceDictionary();
var target = new TestControl { Resources = resources };
Assert.Same(target, ((IResourceNode)resources).ResourceParent);
}
[Fact]
public void Assigning_Resources_Raises_ResourcesChanged()
{
var resources = new ResourceDictionary { { "foo", "bar" } };
var target = new TestControl();
var raised = 0;
target.ResourcesChanged += (s, e) => ++raised;
target.Resources = resources;
Assert.Equal(1, raised);
}
[Fact]
public void Changing_Parent_Notifies_Resources_ParentResourcesChanged()
{
var resources = new Mock<IResourceDictionary>();
var setResourceParent = resources.As<ISetResourceParent>();
var target = new TestControl { Resources = resources.Object };
var parent = new Decorator { Resources = { { "foo", "bar" } } };
setResourceParent.ResetCalls();
parent.Child = target;
setResourceParent.Verify(x =>
x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
[Fact]
public void Styles_Parent_Is_Set()
{
var target = new TestControl();
Assert.Same(target, ((IResourceNode)target.Styles).ResourceParent);
}
[Fact]
public void Changing_Parent_Notifies_Styles_ParentResourcesChanged()
{
var style = new Mock<IStyle>();
var setResourceParent = style.As<ISetResourceParent>();
var target = new TestControl { Styles = { style.Object } };
var parent = new Decorator { Resources = { { "foo", "bar" } } };
setResourceParent.ResetCalls();
parent.Child = target;
setResourceParent.Verify(x =>
x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
[Fact]
public void Changing_Resources_Notifies_Styles()
{
var style = new Mock<IStyle>();
var setResourceParent = style.As<ISetResourceParent>();
var target = new TestControl { Styles = { style.Object } };
setResourceParent.ResetCalls();
target.Resources.Add("foo", "bar");
setResourceParent.Verify(x =>
x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
private interface IDataContextEvents
{
event EventHandler DataContextBeginUpdate;

19
tests/Avalonia.Styling.UnitTests/StyledElementTests_Resources.cs

@ -205,7 +205,7 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
public void Setting_Logical_Parent_Subscribes_To_Parents_ResourceChanged_Event()
public void Setting_Logical_Parent_Raises_Child_ResourcesChanged()
{
var parent = new ContentControl();
var child = new StyledElement();
@ -220,6 +220,23 @@ namespace Avalonia.Controls.UnitTests
Assert.True(raisedOnChild);
}
[Fact]
public void Setting_Logical_Parent_Raises_Style_ResourcesChanged()
{
var style = new Style(x => x.OfType<Canvas>());
var parent = new ContentControl();
var child = new StyledElement { Styles = { style } };
((ISetLogicalParent)child).SetParent(parent);
var raised = false;
style.ResourcesChanged += (_, __) => raised = true;
parent.Resources.Add("foo", "bar");
Assert.True(raised);
}
private IControlTemplate ContentControlTemplate()
{
return new FuncControlTemplate<ContentControl>((x, scope) =>

26
tests/Avalonia.Styling.UnitTests/StylesTests.cs

@ -3,6 +3,7 @@
using System;
using Avalonia.Controls;
using Moq;
using Xunit;
namespace Avalonia.Styling.UnitTests
@ -76,7 +77,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void Adding_Resource_To_Younger_Sibling_Style_Should_Raise_ResourceChanged()
public void Adding_Resource_To_Sibling_Style_Should_Raise_ResourceChanged()
{
Style style1;
Style style2;
@ -95,25 +96,20 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void Adding_Resource_To_Older_Sibling_Style_Should_Raise_ResourceChanged()
public void ParentResourcesChanged_Should_Be_Propagated_To_Children()
{
Style style1;
Style style2;
var target = new Styles
{
(style1 = new Style()),
(style2 = new Style()),
};
var raised = false;
var childStyle = new Mock<IStyle>();
var setResourceParent = childStyle.As<ISetResourceParent>();
var target = new Styles { childStyle.Object };
style1.ResourcesChanged += (_, __) => raised = true;
style2.Resources.Add("foo", "bar");
setResourceParent.ResetCalls();
((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs());
Assert.False(raised);
setResourceParent.Verify(x => x.ParentResourcesChanged(
It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
[Fact]
public void Finds_Resource_In_Merged_Dictionary()
{

Loading…
Cancel
Save