Browse Source

Make dynamic resources work in more situations.

Fixes #492 in a fashion: `DynamicResource` now works for this scenario.
pull/1136/head
Steven Kirk 9 years ago
parent
commit
0e155bd2d4
  1. 3
      src/Avalonia.Controls/Application.cs
  2. 10
      src/Avalonia.Controls/Control.cs
  3. 29
      src/Avalonia.Controls/ControlExtensions.cs
  4. 5
      src/Avalonia.Styling/Controls/IResourceProvider.cs
  5. 49
      src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs
  6. 2
      src/Avalonia.Styling/LogicalTree/ILogical.cs
  7. 32
      src/Avalonia.Styling/Styling/ISetStyleParent.cs
  8. 23
      src/Avalonia.Styling/Styling/Style.cs
  9. 56
      src/Avalonia.Styling/Styling/Styles.cs
  10. 16
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs
  11. 25
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  12. 38
      tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs
  13. 4
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  14. 5
      tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs
  15. 5
      tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs
  16. 115
      tests/Avalonia.Styling.UnitTests/StylesTests.cs

3
src/Avalonia.Controls/Application.cs

@ -128,6 +128,9 @@ namespace Avalonia
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/>
IResourceProvider IResourceProvider.ResourceParent => null;
/// <summary>
/// Initializes the application by loading XAML etc.
/// </summary>

10
src/Avalonia.Controls/Control.cs

@ -283,10 +283,17 @@ namespace Avalonia.Controls
{
if (_styles != null)
{
(_styles as ISetStyleParent)?.SetParent(null);
_styles.ResourcesChanged -= StyleResourcesChanged;
}
_styles = value;
if (value is ISetStyleParent setParent && setParent.ResourceParent == null)
{
setParent.SetParent(this);
}
_styles.ResourcesChanged += StyleResourcesChanged;
}
}
@ -385,6 +392,9 @@ namespace Avalonia.Controls
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources;
/// <inheritdoc/>
IResourceProvider IResourceProvider.ResourceParent => ((IStyleHost)this).StylingParent as IResourceProvider;
/// <inheritdoc/>
IAvaloniaReadOnlyList<string> IStyleable.Classes => Classes;

29
src/Avalonia.Controls/ControlExtensions.cs

@ -81,35 +81,6 @@ namespace Avalonia.Controls
.FirstOrDefault(x => x != null);
}
/// <summary>
/// Finds the specified resource by searching up the logical tree and then global styles.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="key">The resource key.</param>
/// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
public static object FindResource(this IControl control, string key)
{
Contract.Requires<ArgumentNullException>(control != null);
Contract.Requires<ArgumentNullException>(key != null);
var current = control as IStyleHost;
while (current != null)
{
if (current is IResourceProvider host)
{
if (host.TryGetResource(key, out var value))
{
return value;
}
}
current = current.StylingParent;
}
return AvaloniaProperty.UnsetValue;
}
/// <summary>
/// Adds or removes a pseudoclass depending on a boolean value.
/// </summary>

5
src/Avalonia.Styling/Controls/IResourceProvider.cs

@ -17,6 +17,11 @@ namespace Avalonia.Controls
/// </summary>
bool HasResources { get; }
/// <summary>
/// Gets the parent resource provider, if any.
/// </summary>
IResourceProvider ResourceParent { get; }
/// <summary>
/// Tries to find a resource within the element.
/// </summary>

49
src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs

@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
namespace Avalonia.Controls
{
public static class ResourceProviderExtensions
{
/// <summary>
/// Finds the specified resource by searching up the logical tree and then global styles.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="key">The resource key.</param>
/// <returns>The resource, or <see cref="AvaloniaProperty.UnsetValue"/> if not found.</returns>
public static object FindResource(this IResourceProvider control, string key)
{
Contract.Requires<ArgumentNullException>(control != null);
Contract.Requires<ArgumentNullException>(key != null);
var current = control;
while (current != null)
{
if (current is IResourceProvider host)
{
if (host.TryGetResource(key, out var value))
{
return value;
}
}
current = current.ResourceParent;
}
return AvaloniaProperty.UnsetValue;
}
public static IObservable<object> GetResourceObservable(this IResourceProvider target, string key)
{
return Observable.FromEventPattern<ResourcesChangedEventArgs>(
x => target.ResourcesChanged += x,
x => target.ResourcesChanged -= x)
.StartWith((EventPattern<ResourcesChangedEventArgs>)null)
.Select(x => target.FindResource(key));
}
}
}

2
src/Avalonia.Styling/LogicalTree/ILogical.cs

@ -58,7 +58,7 @@ namespace Avalonia.LogicalTree
void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e);
/// <summary>
/// Notifies the control that a change has been made to its resources.
/// Notifies the control that a change has been made to resources that apply to it.
/// </summary>
/// <param name="e">The event args.</param>
/// <remarks>

32
src/Avalonia.Styling/Styling/ISetStyleParent.cs

@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Controls;
namespace Avalonia.Styling
{
/// <summary>
/// Defines an interface through which a <see cref="Style"/>'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
{
/// <summary>
/// Sets the style parent.
/// </summary>
/// <param name="parent">The parent.</param>
void SetParent(IResourceProvider parent);
/// <summary>
/// Notifies the style that a change has been made to resources that apply to it.
/// </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);
}
}

23
src/Avalonia.Styling/Styling/Style.cs

@ -13,10 +13,11 @@ namespace Avalonia.Styling
/// <summary>
/// Defines a style.
/// </summary>
public class Style : IStyle
public class Style : IStyle, ISetStyleParent
{
private static Dictionary<IStyleable, List<IDisposable>> _applied =
new Dictionary<IStyleable, List<IDisposable>>();
private IResourceProvider _parent;
private ResourceDictionary _resources;
/// <summary>
@ -69,6 +70,9 @@ namespace Avalonia.Styling
/// <inheritdoc/>
bool IResourceProvider.HasResources => _resources?.Count > 0;
/// <inheritdoc/>
IResourceProvider IResourceProvider.ResourceParent => _parent;
/// <summary>
/// Attaches the style to a control if the style's selector matches.
/// </summary>
@ -128,6 +132,23 @@ namespace Avalonia.Styling
}
}
/// <inheritdoc/>
void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
}
/// <inheritdoc/>
void ISetStyleParent.SetParent(IResourceProvider parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The Style already has a parent.");
}
_parent = parent;
}
private static List<IDisposable> GetSubscriptions(IStyleable control)
{
List<IDisposable> subscriptions;

56
src/Avalonia.Styling/Styling/Styles.cs

@ -12,8 +12,9 @@ namespace Avalonia.Styling
/// <summary>
/// A style that consists of a number of child styles.
/// </summary>
public class Styles : AvaloniaList<IStyle>, IStyle
public class Styles : AvaloniaList<IStyle>, IStyle, ISetStyleParent
{
private IResourceProvider _parent;
private ResourceDictionary _resources;
public Styles()
@ -22,6 +23,12 @@ namespace Avalonia.Styling
this.ForEachItem(
x =>
{
if (x.ResourceParent == null && x is ISetStyleParent setParent)
{
setParent.SetParent(this);
setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
@ -31,6 +38,12 @@ namespace Avalonia.Styling
},
x =>
{
if (x.ResourceParent == this && x is ISetStyleParent setParent)
{
setParent.SetParent(null);
setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs());
}
if (x.HasResources)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
@ -64,6 +77,9 @@ namespace Avalonia.Styling
}
}
/// <inheritdoc/>
IResourceProvider IResourceProvider.ResourceParent => _parent;
/// <summary>
/// Attaches the style to a control if the style's selector matches.
/// </summary>
@ -99,13 +115,49 @@ namespace Avalonia.Styling
return false;
}
/// <inheritdoc/>
void ISetStyleParent.SetParent(IResourceProvider parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The Style already has a parent.");
}
_parent = parent;
}
/// <inheritdoc/>
void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, e);
}
private void ResourceDictionaryChanged(object sender, NotifyCollectionChangedEventArgs e)
{
ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs());
var ev = new ResourcesChangedEventArgs();
foreach (var child in this)
{
(child as ISetStyleParent)?.NotifyResourcesChanged(ev);
}
ResourcesChanged?.Invoke(this, ev);
}
private void SubResourceChanged(object sender, ResourcesChangedEventArgs e)
{
var foundSource = false;
foreach (var child in this)
{
if (foundSource)
{
(child as ISetStyleParent)?.NotifyResourcesChanged(e);
}
foundSource |= child == sender;
}
ResourcesChanged?.Invoke(this, e);
}
}

16
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs

@ -15,7 +15,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
{
public class DynamicResourceExtension : MarkupExtension, IBinding
{
private IControl _anchor;
private IResourceProvider _anchor;
public DynamicResourceExtension()
{
@ -33,9 +33,9 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
var context = (ITypeDescriptorContext)serviceProvider;
var provideTarget = context.GetService<IProvideValueTarget>();
if (!(provideTarget.TargetObject is IControl))
if (!(provideTarget.TargetObject is IResourceProvider))
{
_anchor = GetAnchor<IControl>(context);
_anchor = GetAnchor<IResourceProvider>(context);
}
return this;
@ -47,17 +47,11 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
object anchor,
bool enableDataValidation)
{
var control = target as IControl ?? _anchor as IControl;
var control = target as IResourceProvider ?? _anchor;
if (control != null)
{
var o = Observable.FromEventPattern<ResourcesChangedEventArgs>(
x => control.ResourcesChanged += x,
x => control.ResourcesChanged -= x)
.StartWith((EventPattern<ResourcesChangedEventArgs>)null)
.Select(x => control.FindResource(ResourceKey));
return new InstancedBinding(o);
return new InstancedBinding(control.GetResourceObservable(ResourceKey));
}
return null;

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

@ -10,16 +10,16 @@ namespace Avalonia.Markup.Xaml.Styling
/// <summary>
/// Includes a style from a URL.
/// </summary>
public class StyleInclude : IStyle
public class StyleInclude : IStyle, ISetStyleParent
{
private Uri _baseUri;
private IStyle _loaded;
private IResourceProvider _parent;
/// <summary>
/// Initializes a new instance of the <see cref="StyleInclude"/> class.
/// </summary>
/// <param name="baseUri"></param>
public StyleInclude(Uri baseUri)
{
_baseUri = baseUri;
@ -44,6 +44,7 @@ namespace Avalonia.Markup.Xaml.Styling
{
var loader = new AvaloniaXamlLoader();
_loaded = (IStyle)loader.Load(Source, _baseUri);
(_loaded as ISetStyleParent)?.SetParent(this);
}
return _loaded;
@ -53,6 +54,9 @@ namespace Avalonia.Markup.Xaml.Styling
/// <inheritdoc/>
bool IResourceProvider.HasResources => Loaded.HasResources;
/// <inheritdoc/>
IResourceProvider IResourceProvider.ResourceParent => _parent;
/// <inheritdoc/>
public void Attach(IStyleable control, IStyleHost container)
{
@ -64,5 +68,22 @@ namespace Avalonia.Markup.Xaml.Styling
/// <inheritdoc/>
public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value);
/// <inheritdoc/>
void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
(Loaded as ISetStyleParent)?.NotifyResourcesChanged(e);
}
/// <inheritdoc/>
void ISetStyleParent.SetParent(IResourceProvider parent)
{
if (_parent != null && parent != null)
{
throw new InvalidOperationException("The Style already has a parent.");
}
_parent = parent;
}
}
}

38
tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs

@ -204,44 +204,6 @@ namespace Avalonia.Controls.UnitTests
Assert.True(raised);
}
[Fact]
public void Adding_Style_With_Resource_Should_Raise_ResourceChanged()
{
Style style = new Style
{
Resources = { { "foo", "bar" } },
};
var target = new Decorator();
var raised = false;
target.ResourcesChanged += (_, __) => raised = true;
target.Styles.Add(style);
Assert.True(raised);
}
[Fact]
public void Removing_Style_With_Resource_Should_Raise_ResourceChanged()
{
var target = new Decorator
{
Styles =
{
new Style
{
Resources = { { "foo", "bar" } },
}
}
};
var raised = false;
target.ResourcesChanged += (_, __) => raised = true;
target.Styles.Clear();
Assert.True(raised);
}
private IControlTemplate ContentControlTemplate()
{
return new FuncControlTemplate<ContentControl>(x =>

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

@ -360,8 +360,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<Color x:Key='Red'>Red</Color>
<Color x:Key='Green'>Green</Color>
<Color x:Key='Blue'>Blue</Color>
</Style.Resources>
</Style>";
var style2Xaml = @"
@ -369,8 +367,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Style.Resources>
<SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
<SolidColorBrush x:Key='GreenBrush' Color='{DynamicResource Green}'/>
<SolidColorBrush x:Key='BlueBrush' Color='{DynamicResource Blue}'/>
</Style.Resources>
</Style>";
using (StyledWindow(

5
tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs

@ -154,6 +154,11 @@ namespace Avalonia.Styling.UnitTests
{
throw new NotImplementedException();
}
public void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
throw new NotImplementedException();
}
}
public class TestLogical1 : TestLogical

5
tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs

@ -184,6 +184,11 @@ namespace Avalonia.Styling.UnitTests
{
throw new NotImplementedException();
}
public void NotifyResourcesChanged(ResourcesChangedEventArgs e)
{
throw new NotImplementedException();
}
}
public class TestLogical1 : TestLogical

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

@ -0,0 +1,115 @@
// 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 Xunit;
namespace Avalonia.Styling.UnitTests
{
public class StylesTests
{
[Fact]
public void Adding_Style_With_Resources_Should_Raise_ResourceChanged()
{
var style = new Style
{
Resources = { { "foo", "bar" } },
};
var target = new Styles();
var raised = false;
target.ResourcesChanged += (_, __) => raised = true;
target.Add(style);
Assert.True(raised);
}
[Fact]
public void Removing_Style_With_Resources_Should_Raise_ResourceChanged()
{
var target = new Styles
{
new Style
{
Resources = { { "foo", "bar" } },
}
};
var raised = false;
target.ResourcesChanged += (_, __) => raised = true;
target.Clear();
Assert.True(raised);
}
[Fact]
public void Adding_Style_Without_Resources_Should_Not_Raise_ResourceChanged()
{
var style = new Style();
var target = new Styles();
var raised = false;
target.ResourcesChanged += (_, __) => raised = true;
target.Add(style);
Assert.False(raised);
}
[Fact]
public void Adding_Resource_Should_Raise_Child_ResourceChanged()
{
Style child;
var target = new Styles
{
(child = new Style()),
};
var raised = false;
child.ResourcesChanged += (_, __) => raised = true;
target.Resources.Add("foo", "bar");
Assert.True(raised);
}
[Fact]
public void Adding_Resource_To_Younger_Sibling_Style_Should_Raise_ResourceChanged()
{
Style style1;
Style style2;
var target = new Styles
{
(style1 = new Style()),
(style2 = new Style()),
};
var raised = false;
style2.ResourcesChanged += (_, __) => raised = true;
style1.Resources.Add("foo", "bar");
Assert.True(raised);
}
[Fact]
public void Adding_Resource_To_Older_Sibling_Style_Should_Raise_ResourceChanged()
{
Style style1;
Style style2;
var target = new Styles
{
(style1 = new Style()),
(style2 = new Style()),
};
var raised = false;
style1.ResourcesChanged += (_, __) => raised = true;
style2.Resources.Add("foo", "bar");
Assert.False(raised);
}
}
}
Loading…
Cancel
Save