Browse Source

Merge pull request #3327 from AvaloniaUI/fixes/3323-resourcedictionary-resource

Fix referencing resources in merged dictionaries
release/0.9.2
Jumar Macato 6 years ago
committed by Dan Walmsley
parent
commit
3469d23272
  1. 14
      src/Avalonia.Styling/Controls/ISetResourceParent.cs
  2. 62
      src/Avalonia.Styling/Controls/ResourceDictionary.cs
  3. 4
      src/Avalonia.Styling/StyledElement.cs
  4. 12
      src/Avalonia.Styling/Styling/Style.cs
  5. 20
      src/Avalonia.Styling/Styling/Styles.cs
  6. 26
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs
  7. 10
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  8. 123
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs
  9. 45
      tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs

14
src/Avalonia.Styling/Styling/ISetStyleParent.cs → src/Avalonia.Styling/Controls/ISetResourceParent.cs

@ -1,29 +1,27 @@
using Avalonia.Controls;
namespace Avalonia.Styling
namespace Avalonia.Controls
{
/// <summary>
/// Defines an interface through which a <see cref="Style"/>'s parent can be set.
/// Defines an interface through which an <see cref="IResourceNode"/>'s parent can be set.
/// </summary>
/// <remarks>
/// You should not usually need to use this interface - it is for internal use only.
/// </remarks>
public interface ISetStyleParent : IStyle
public interface ISetResourceParent : IResourceNode
{
/// <summary>
/// Sets the style parent.
/// Sets the resource parent.
/// </summary>
/// <param name="parent">The parent.</param>
void SetParent(IResourceNode parent);
/// <summary>
/// Notifies the style that a change has been made to resources that apply to it.
/// Notifies the resource node that a change has been made to the resources in its parent.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>
/// This method will be called automatically by the framework, you should not need to call
/// this method yourself.
/// </remarks>
void NotifyResourcesChanged(ResourcesChangedEventArgs e);
void ParentResourcesChanged(ResourcesChangedEventArgs e);
}
}

62
src/Avalonia.Styling/Controls/ResourceDictionary.cs

@ -12,8 +12,12 @@ namespace Avalonia.Controls
/// <summary>
/// An indexed dictionary of resources.
/// </summary>
public class ResourceDictionary : AvaloniaDictionary<object, object>, IResourceDictionary
public class ResourceDictionary : AvaloniaDictionary<object, object>,
IResourceDictionary,
IResourceNode,
ISetResourceParent
{
private IResourceNode _parent;
private AvaloniaList<IResourceProvider> _mergedDictionaries;
/// <summary>
@ -39,6 +43,12 @@ namespace Avalonia.Controls
_mergedDictionaries.ForEachItem(
x =>
{
if (x is ISetResourceParent setParent)
{
setParent.SetParent(this);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{
OnResourcesChanged();
@ -48,11 +58,18 @@ namespace Avalonia.Controls
},
x =>
{
if (x is ISetResourceParent setParent)
{
setParent.SetParent(null);
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{
OnResourcesChanged();
}
(x as ISetResourceParent)?.SetParent(null);
x.ResourcesChanged -= MergedDictionaryResourcesChanged;
},
() => { });
@ -68,6 +85,27 @@ namespace Avalonia.Controls
get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false);
}
/// <inheritdoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
/// <inheritdoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{
NotifyMergedDictionariesResourcesChanged(e);
ResourcesChanged?.Invoke(this, e);
}
/// <inheritdoc/>
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The ResourceDictionary already has a parent.");
}
_parent = parent;
}
/// <inheritdoc/>
public bool TryGetResource(object key, out object value)
{
@ -95,7 +133,27 @@ namespace Avalonia.Controls
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged();
private void NotifyMergedDictionariesResourcesChanged(ResourcesChangedEventArgs e)
{
if (_mergedDictionaries != null)
{
for (var i = _mergedDictionaries.Count - 1; i >= 0; --i)
{
if (_mergedDictionaries[i] is ISetResourceParent merged)
{
merged.ParentResourcesChanged(e);
}
}
}
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
var ev = new ResourcesChangedEventArgs();
NotifyMergedDictionariesResourcesChanged(ev);
OnResourcesChanged();
}
private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged();
}
}

4
src/Avalonia.Styling/StyledElement.cs

@ -223,13 +223,13 @@ namespace Avalonia
{
if (_styles != null)
{
(_styles as ISetStyleParent)?.SetParent(null);
(_styles as ISetResourceParent)?.SetParent(null);
_styles.ResourcesChanged -= ThisResourcesChanged;
}
_styles = value;
if (value is ISetStyleParent setParent && setParent.ResourceParent == null)
if (value is ISetResourceParent setParent && setParent.ResourceParent == null)
{
setParent.SetParent(this);
}

12
src/Avalonia.Styling/Styling/Style.cs

@ -14,7 +14,7 @@ namespace Avalonia.Styling
/// <summary>
/// Defines a style.
/// </summary>
public class Style : AvaloniaObject, IStyle, ISetStyleParent
public class Style : AvaloniaObject, IStyle, ISetResourceParent
{
private static Dictionary<IStyleable, CompositeDisposable> _applied =
new Dictionary<IStyleable, CompositeDisposable>();
@ -59,16 +59,16 @@ namespace Avalonia.Styling
if (_resources != null)
{
hadResources = _resources.Count > 0;
hadResources = _resources.HasResources;
_resources.ResourcesChanged -= ResourceDictionaryChanged;
}
_resources = value;
_resources.ResourcesChanged += ResourceDictionaryChanged;
if (hadResources || _resources.Count > 0)
if (hadResources || _resources.HasResources)
{
((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs());
}
}
}
@ -194,13 +194,13 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
}
/// <inheritdoc/>
void ISetStyleParent.SetParent(IResourceNode parent)
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{

20
src/Avalonia.Styling/Styling/Styles.cs

@ -14,7 +14,7 @@ namespace Avalonia.Styling
/// <summary>
/// A style that consists of a number of child styles.
/// </summary>
public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetStyleParent
public class Styles : AvaloniaObject, IAvaloniaList<IStyle>, IStyle, ISetResourceParent
{
private IResourceNode _parent;
private IResourceDictionary _resources;
@ -27,10 +27,10 @@ namespace Avalonia.Styling
_styles.ForEachItem(
x =>
{
if (x.ResourceParent == null && x is ISetStyleParent setParent)
if (x.ResourceParent == null && x is ISetResourceParent setParent)
{
setParent.SetParent(this);
setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
@ -43,10 +43,10 @@ namespace Avalonia.Styling
},
x =>
{
if (x.ResourceParent == this && x is ISetStyleParent setParent)
if (x.ResourceParent == this && x is ISetResourceParent setParent)
{
setParent.SetParent(null);
setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
setParent.ParentResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
@ -98,7 +98,7 @@ namespace Avalonia.Styling
if (hadResources || _resources.Count > 0)
{
((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs());
((ISetResourceParent)this).ParentResourcesChanged(new ResourcesChangedEventArgs());
}
}
}
@ -246,7 +246,7 @@ namespace Avalonia.Styling
IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator();
/// <inheritdoc/>
void ISetStyleParent.SetParent(IResourceNode parent)
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{
@ -257,7 +257,7 @@ namespace Avalonia.Styling
}
/// <inheritdoc/>
void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
}
@ -266,7 +266,7 @@ namespace Avalonia.Styling
{
foreach (var child in this)
{
(child as ISetStyleParent)?.NotifyResourcesChanged(e);
(child as ISetResourceParent)?.ParentResourcesChanged(e);
}
ResourcesChanged?.Invoke(this, e);
@ -280,7 +280,7 @@ namespace Avalonia.Styling
{
if (foundSource)
{
(child as ISetStyleParent)?.NotifyResourcesChanged(e);
(child as ISetResourceParent)?.ParentResourcesChanged(e);
}
foundSource |= child == sender;

26
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs

@ -7,8 +7,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
/// <summary>
/// Loads a resource dictionary from a specified URL.
/// </summary>
public class ResourceInclude :IResourceProvider
public class ResourceInclude : IResourceNode, ISetResourceParent
{
private IResourceNode _parent;
private Uri _baseUri;
private IResourceDictionary _loaded;
@ -26,6 +27,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
var loader = new AvaloniaXamlLoader();
_loaded = (IResourceDictionary)loader.Load(Source, _baseUri);
(_loaded as ISetResourceParent)?.SetParent(this);
_loaded.ResourcesChanged += ResourcesChanged;
if (_loaded.HasResources)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
@ -44,12 +48,32 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
/// <inhertidoc/>
bool IResourceProvider.HasResources => Loaded.HasResources;
/// <inhertidoc/>
IResourceNode IResourceNode.ResourceParent => _parent;
/// <inhertidoc/>
bool IResourceProvider.TryGetResource(object key, out object value)
{
return Loaded.TryGetResource(key, out value);
}
/// <inhertidoc/>
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The ResourceInclude already has a parent.");
}
_parent = parent;
}
/// <inhertidoc/>
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{
(_loaded as ISetResourceParent)?.ParentResourcesChanged(e);
}
public ResourceInclude ProvideValue(IServiceProvider serviceProvider)
{
var tdc = (ITypeDescriptorContext)serviceProvider;

10
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@ -10,7 +10,7 @@ namespace Avalonia.Markup.Xaml.Styling
/// <summary>
/// Includes a style from a URL.
/// </summary>
public class StyleInclude : IStyle, ISetStyleParent
public class StyleInclude : IStyle, ISetResourceParent
{
private Uri _baseUri;
private IStyle _loaded;
@ -53,7 +53,7 @@ namespace Avalonia.Markup.Xaml.Styling
{
var loader = new AvaloniaXamlLoader();
_loaded = (IStyle)loader.Load(Source, _baseUri);
(_loaded as ISetStyleParent)?.SetParent(this);
(_loaded as ISetResourceParent)?.SetParent(this);
}
return _loaded;
@ -89,13 +89,13 @@ namespace Avalonia.Markup.Xaml.Styling
public bool TryGetResource(object key, out object value) => Loaded.TryGetResource(key, out value);
/// <inheritdoc/>
void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
void ISetResourceParent.ParentResourcesChanged(ResourcesChangedEventArgs e)
{
(Loaded as ISetStyleParent)?.NotifyResourcesChanged(e);
(Loaded as ISetResourceParent)?.ParentResourcesChanged(e);
}
/// <inheritdoc/>
void ISetStyleParent.SetParent(IResourceNode parent)
void ISetResourceParent.SetParent(IResourceNode parent)
{
if (_parent != null && parent != null)
{

123
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@ -0,0 +1,123 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
{
public class ResourceDictionaryTests : XamlTestBase
{
[Fact]
public void StaticResource_Works_In_ResourceDictionary()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Color x:Key='Red'>Red</Color>
<SolidColorBrush x:Key='RedBrush' Color='{StaticResource Red}'/>
</ResourceDictionary>";
var loader = new AvaloniaXamlLoader();
var resources = (ResourceDictionary)loader.Load(xaml);
var brush = (SolidColorBrush)resources["RedBrush"];
Assert.Equal(Colors.Red, brush.Color);
}
}
[Fact]
public void DynamicResource_Works_In_ResourceDictionary()
{
using (StyledWindow())
{
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Color x:Key='Red'>Red</Color>
<SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
</ResourceDictionary>";
var loader = new AvaloniaXamlLoader();
var resources = (ResourceDictionary)loader.Load(xaml);
var brush = (SolidColorBrush)resources["RedBrush"];
Assert.Equal(Colors.Red, brush.Color);
}
}
[Fact]
public void DynamicResource_Finds_Resource_In_Parent_Dictionary()
{
var dictionaryXaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
</ResourceDictionary>";
using (StyledWindow(assets: ("test:dict.xaml", dictionaryXaml)))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source='test:dict.xaml'/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
<Color x:Key='Red'>Red</Color>
</Window.Resources>
<Button Name='button' Background='{DynamicResource RedBrush}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
var brush = Assert.IsType<SolidColorBrush>(button.Background);
Assert.Equal(Colors.Red, brush.Color);
window.Resources["Red"] = Colors.Green;
Assert.Equal(Colors.Green, brush.Color);
}
}
private IDisposable StyledWindow(params (string, string)[] assets)
{
var services = TestServices.StyledWindow.With(
assetLoader: new MockAssetLoader(assets),
theme: () => new Styles
{
WindowStyle(),
});
return UnitTestApplication.Start(services);
}
private Style WindowStyle()
{
return new Style(x => x.OfType<Window>())
{
Setters =
{
new Setter(
Window.TemplateProperty,
new FuncControlTemplate<Window>((x, scope) =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
[!ContentPresenter.ContentProperty] = x[!Window.ContentProperty],
}.RegisterInNameScope(scope)))
}
};
}
}
}

45
tests/Avalonia.Styling.UnitTests/ResourceDictionaryTests.cs

@ -3,6 +3,7 @@
using System;
using Avalonia.Controls;
using Moq;
using Xunit;
namespace Avalonia.Styling.UnitTests
@ -136,7 +137,7 @@ namespace Avalonia.Styling.UnitTests
}
[Fact]
public void ResourcesChanged_Should_Not_Be_Raised_On_Empty_MergedDictionary_Remove()
public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add()
{
var target = new ResourceDictionary
{
@ -145,31 +146,45 @@ namespace Avalonia.Styling.UnitTests
new ResourceDictionary(),
}
};
var raised = false;
target.ResourcesChanged += (_, __) => raised = true;
target.MergedDictionaries.RemoveAt(0);
((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
Assert.False(raised);
Assert.True(raised);
}
[Fact]
public void ResourcesChanged_Should_Be_Raised_On_MergedDictionary_Resource_Add()
public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_Resource_Add()
{
var target = new ResourceDictionary
{
MergedDictionaries =
{
new ResourceDictionary(),
}
};
var target = new ResourceDictionary();
var merged = new Mock<ISetResourceParent>();
var raised = false;
target.MergedDictionaries.Add(merged.Object);
merged.ResetCalls();
target.ResourcesChanged += (_, __) => raised = true;
((IResourceDictionary)target.MergedDictionaries[0]).Add("foo", "bar");
target.Add("foo", "bar");
Assert.True(raised);
merged.Verify(
x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
[Fact]
public void MergedDictionary_ParentResourcesChanged_Should_Be_Called_On_NotifyResourceChanged()
{
var target = new ResourceDictionary();
var merged = new Mock<ISetResourceParent>();
target.MergedDictionaries.Add(merged.Object);
merged.ResetCalls();
((ISetResourceParent)target).ParentResourcesChanged(new ResourcesChangedEventArgs());
merged.Verify(
x => x.ParentResourcesChanged(It.IsAny<ResourcesChangedEventArgs>()),
Times.Once);
}
}
}

Loading…
Cancel
Save