From 84aa27162f33bc80d60a66ad4eca57a1435d2ad4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Aug 2017 02:59:41 +0200 Subject: [PATCH] Made a start adding Control/Application.Resources. --- samples/ControlCatalog/MainWindow.xaml.cs | 2 +- src/Avalonia.Controls/Application.cs | 19 +++- src/Avalonia.Controls/Control.cs | 15 ++++ src/Avalonia.Controls/ControlExtensions.cs | 29 +++++++ src/Avalonia.Controls/IControl.cs | 2 +- .../Presenters/TextPresenter.cs | 2 +- src/Avalonia.Styling/Avalonia.Styling.csproj | 1 + .../Controls/IResourceDictionary.cs | 17 +++- .../Controls/IResourceHost.cs | 22 +++++ .../Controls/ResourceDictionary.cs | 2 + src/Avalonia.Styling/Styling/IStyle.cs | 13 +-- src/Avalonia.Styling/Styling/IStyleHost.cs | 1 - src/Avalonia.Styling/Styling/Style.cs | 22 +---- .../Styling/StyleExtensions.cs | 39 --------- src/Avalonia.Styling/Styling/Styles.cs | 40 ++++++--- .../Data/StyleResourceBinding.cs | 7 +- .../Styling/StyleInclude.cs | 14 +-- .../ControlTests_Resources.cs | 87 +++++++++++++++++++ .../FullLayoutTests.cs | 4 + .../Xaml/BasicTests.cs | 6 +- .../Xaml/StyleTests.cs | 12 +-- .../ResourceTests.cs | 78 ----------------- tests/Avalonia.UnitTests/TestRoot.cs | 4 + 23 files changed, 249 insertions(+), 189 deletions(-) create mode 100644 src/Avalonia.Styling/Controls/IResourceHost.cs delete mode 100644 src/Avalonia.Styling/Styling/StyleExtensions.cs create mode 100644 tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs delete mode 100644 tests/Avalonia.Styling.UnitTests/ResourceTests.cs diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index 413794dfa2..0d24967482 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -19,7 +19,7 @@ namespace ControlCatalog // so we must refer to this resource DLL statically. For // now I am doing that here. But we need a better solution!! var theme = new Avalonia.Themes.Default.DefaultTheme(); - theme.FindResource("Button"); + theme.TryGetResource("Button"); AvaloniaXamlLoader.Load(this); } } diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 3d13608226..0d89ad65b1 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -29,7 +29,7 @@ namespace Avalonia /// method. /// - Tracks the lifetime of the application. /// - public class Application : IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IApplicationLifecycle + public class Application : IApplicationLifecycle, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceHost { /// /// The application-global data templates. @@ -39,6 +39,7 @@ namespace Avalonia private readonly Lazy _clipboard = new Lazy(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))); private readonly Styler _styler = new Styler(); + private ResourceDictionary _resources; /// /// Initializes a new instance of the class. @@ -100,6 +101,11 @@ namespace Avalonia /// public IClipboard Clipboard => _clipboard.Value; + /// + /// Gets the application's global resource dictionary. + /// + public IResourceDictionary Resources => _resources ?? (_resources = new ResourceDictionary()); + /// /// Gets the application's global styles. /// @@ -142,13 +148,20 @@ namespace Avalonia { OnExit?.Invoke(this, EventArgs.Empty); } - + + /// + bool IResourceHost.TryGetResource(string key, out object value) + { + value = null; + return _resources?.TryGetResource(key, out value) ?? + Styles.TryGetResource(key, out value); + } + /// /// Sent when the application is exiting. /// public event EventHandler OnExit; - /// /// Called when the application is exiting. /// diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index eca5967a58..ab5d9a7f06 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -97,6 +97,7 @@ namespace Avalonia.Controls private bool _isAttachedToLogicalTree; private IAvaloniaList _logicalChildren; private INameScope _nameScope; + private ResourceDictionary _resources; private Styles _styles; private bool _styled; private Subject _styleDetach = new Subject(); @@ -286,6 +287,11 @@ namespace Avalonia.Controls set { SetValue(ContextMenuProperty, value); } } + /// + /// Gets or sets the control's resource dictionary. + /// + public IResourceDictionary Resources => _resources ?? (_resources = new ResourceDictionary()); + /// /// Gets or sets a user-defined object attached to the control. /// @@ -418,6 +424,15 @@ namespace Avalonia.Controls /// protected IPseudoClasses PseudoClasses => Classes; + /// + bool IResourceHost.TryGetResource(string key, out object value) + { + value = null; + return _resources?.TryGetResource(key, out value) ?? + _styles?.TryGetResource(key, out value) ?? + false; + } + /// /// Sets the control's logical parent. /// diff --git a/src/Avalonia.Controls/ControlExtensions.cs b/src/Avalonia.Controls/ControlExtensions.cs index 60a940627f..d0b8b75e7b 100644 --- a/src/Avalonia.Controls/ControlExtensions.cs +++ b/src/Avalonia.Controls/ControlExtensions.cs @@ -81,6 +81,35 @@ namespace Avalonia.Controls .FirstOrDefault(x => x != null); } + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// The resource, or null if not found. + public static object FindResource(this IControl control, string key) + { + Contract.Requires(control != null); + Contract.Requires(key != null); + + var current = control as IStyleHost; + + while (current != null) + { + if (current is IResourceHost host) + { + if (host.TryGetResource(key, out var value)) + { + return value; + } + } + + current = current.StylingParent; + } + + return null; + } + /// /// Adds or removes a pseudoclass depending on a boolean value. /// diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index 3f5bd3fcac..02973bb38d 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls /// /// Interface for Avalonia controls. /// - public interface IControl : IVisual, ILogical, ILayoutable, IInputElement, INamed, IStyleable, IStyleHost + public interface IControl : IVisual, ILogical, ILayoutable, IInputElement, INamed, IResourceHost, IStyleable, IStyleHost { /// /// Occurs when the control has finished initialization. diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index a2b5f3f8b4..5f6b3ad4c8 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -115,7 +115,7 @@ namespace Avalonia.Controls.Presenters if (_highlightBrush == null) { - _highlightBrush = (IBrush)this.FindStyleResource("HighlightBrush"); + _highlightBrush = (IBrush)this.FindResource("HighlightBrush"); } foreach (var rect in rects) diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index 6bf37b522b..6b606616bb 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -2,6 +2,7 @@ netstandard1.3 false + Avalonia true diff --git a/src/Avalonia.Styling/Controls/IResourceDictionary.cs b/src/Avalonia.Styling/Controls/IResourceDictionary.cs index 9891249568..bc76d8f60e 100644 --- a/src/Avalonia.Styling/Controls/IResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/IResourceDictionary.cs @@ -1,5 +1,7 @@ -using System; -using System.Collections; +// 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 System.Collections.Generic; namespace Avalonia.Controls @@ -9,5 +11,16 @@ namespace Avalonia.Controls /// public interface IResourceDictionary : IDictionary { + /// + /// Tries to find a resource within the dictionary. + /// + /// The resource key. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the key is found; otherwise, null + /// + /// True if the resource if found, otherwise false. + /// + bool TryGetResource(string key, out object value); } } diff --git a/src/Avalonia.Styling/Controls/IResourceHost.cs b/src/Avalonia.Styling/Controls/IResourceHost.cs new file mode 100644 index 0000000000..6cee7083e0 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceHost.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Defines an element that can be queried for resources. + /// + public interface IResourceHost + { + /// + /// Tries to find a resource within the element. + /// + /// The resource key. + /// + /// When this method returns, contains the value associated with the specified key, + /// if the key is found; otherwise, null + /// + /// True if the resource if found, otherwise false. + /// + bool TryGetResource(string key, out object value); + } +} diff --git a/src/Avalonia.Styling/Controls/ResourceDictionary.cs b/src/Avalonia.Styling/Controls/ResourceDictionary.cs index 125bb0dcdf..58dee17775 100644 --- a/src/Avalonia.Styling/Controls/ResourceDictionary.cs +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -55,6 +55,8 @@ namespace Avalonia.Controls public bool TryGetValue(string key, out object value) => _inner.TryGetValue(key, out value); + public bool TryGetResource(string key, out object value) => _inner.TryGetValue(key, out value); + bool ICollection>.Contains(KeyValuePair item) { return ((IDictionary)_inner).Contains(item); diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index 7b8510fe2d..70f6c60d14 100644 --- a/src/Avalonia.Styling/Styling/IStyle.cs +++ b/src/Avalonia.Styling/Styling/IStyle.cs @@ -1,12 +1,14 @@ // 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 Avalonia.Controls; + namespace Avalonia.Styling { /// /// Defines the interface for styles. /// - public interface IStyle + public interface IStyle : IResourceHost { /// /// Attaches the style to a control if the style's selector matches. @@ -16,14 +18,5 @@ namespace Avalonia.Styling /// The control that contains this style. May be null. /// void Attach(IStyleable control, IStyleHost container); - - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - object FindResource(string name); } } diff --git a/src/Avalonia.Styling/Styling/IStyleHost.cs b/src/Avalonia.Styling/Styling/IStyleHost.cs index 8422f18b46..466edc4423 100644 --- a/src/Avalonia.Styling/Styling/IStyleHost.cs +++ b/src/Avalonia.Styling/Styling/IStyleHost.cs @@ -17,6 +17,5 @@ namespace Avalonia.Styling /// Gets the parent style host element. /// IStyleHost StylingParent { get; } - } } diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 80238889c8..5dc0409cf0 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -98,25 +98,11 @@ namespace Avalonia.Styling } } - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public object FindResource(string name) + /// + public bool TryGetResource(string key, out object result) { - object result = null; - - if (_resources?.TryGetValue(name, out result) == true) - { - return result; - } - else - { - return AvaloniaProperty.UnsetValue; - } + result = null; + return _resources?.TryGetResource(key, out result) ?? false; } /// diff --git a/src/Avalonia.Styling/Styling/StyleExtensions.cs b/src/Avalonia.Styling/Styling/StyleExtensions.cs deleted file mode 100644 index e1073335a0..0000000000 --- a/src/Avalonia.Styling/Styling/StyleExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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; - -namespace Avalonia.Styling -{ - public static class StyleExtensions - { - /// - /// Tries to find a named style resource. - /// - /// The control from which to find the resource. - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public static object FindStyleResource(this IStyleHost control, string name) - { - Contract.Requires(control != null); - Contract.Requires(name != null); - Contract.Requires(!string.IsNullOrWhiteSpace(name)); - - while (control != null) - { - var result = control.Styles.FindResource(name); - - if (result != AvaloniaProperty.UnsetValue) - { - return result; - } - - control = control.StylingParent; - } - - return AvaloniaProperty.UnsetValue; - } - } -} diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 770ef8344f..75b8fd0c0c 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -3,6 +3,7 @@ using System.Linq; using Avalonia.Collections; +using Avalonia.Controls; namespace Avalonia.Styling { @@ -11,6 +12,24 @@ namespace Avalonia.Styling /// public class Styles : AvaloniaList, IStyle { + private IResourceDictionary _resources; + + /// + /// Gets or sets a dictionary of style resources. + /// + public IResourceDictionary Resources + { + get + { + if (_resources == null) + { + _resources = new ResourceDictionary(); + } + + return _resources; + } + } + /// /// Attaches the style to a control if the style's selector matches. /// @@ -26,26 +45,19 @@ namespace Avalonia.Styling } } - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public object FindResource(string name) + /// + public bool TryGetResource(string key, out object value) { - foreach (var style in this.Reverse()) + for (var i = Count - 1; i >= 0; --i) { - var result = style.FindResource(name); - - if (result != AvaloniaProperty.UnsetValue) + if (this[i].TryGetResource(key, out value)) { - return result; + return true; } } - return AvaloniaProperty.UnsetValue; + value = null; + return false; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs index c1a895f797..787aebbdc6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs @@ -46,11 +46,14 @@ namespace Avalonia.Markup.Xaml.Data if (host != null) { - resource = host.FindStyleResource(Name); + resource = host.FindResource(Name); } else if (style != null) { - resource = style.FindResource(Name); + if (!style.TryGetResource(Name, out resource)) + { + resource = AvaloniaProperty.UnsetValue; + } } if (resource != AvaloniaProperty.UnsetValue) diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 0f4824d493..a8fecdc6e1 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -3,6 +3,7 @@ using Avalonia.Styling; using System; +using Avalonia.Controls; namespace Avalonia.Markup.Xaml.Styling { @@ -55,16 +56,7 @@ namespace Avalonia.Markup.Xaml.Styling } } - /// - /// Tries to find a named resource within the style. - /// - /// The resource name. - /// - /// The resource if found, otherwise . - /// - public object FindResource(string name) - { - return Loaded.FindResource(name); - } + /// + public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value); } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs new file mode 100644 index 0000000000..361806c0a8 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs @@ -0,0 +1,87 @@ +// 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.Styling; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ControlTests_Resources + { + [Fact] + public void FindResource_Should_Find_Control_Resource() + { + var target = new Control + { + Resources = + { + { "foo", "foo-value" }, + } + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Control_Resource_In_Parent() + { + Control target; + + var root = new Decorator + { + Resources = + { + { "foo", "foo-value" }, + }, + Child = target = new Control(), + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Application_Resource() + { + Control target; + + var app = new Application + { + Resources = + { + { "foo", "foo-value" }, + }, + }; + + var root = new TestRoot + { + Child = target = new Control(), + StylingParent = app, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Style_Resource() + { + var target = new Control + { + Styles = + { + new Style + { + Resources = + { + { "foo", "foo-value" }, + } + } + } + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + } +} diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 6b7c73da2a..006306a3e4 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -162,6 +162,10 @@ namespace Avalonia.Layout.UnitTests private void RegisterServices() { var globalStyles = new Mock(); + var globalStylesResources = globalStyles.As(); + var outObj = (object)10; + globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true); + var renderInterface = new Mock(); renderInterface.Setup(x => x.CreateFormattedText( diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 318a98ad43..ce370790c3 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -480,13 +480,13 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.True(style.Resources.Count > 0); - var brush = style.FindResource("Brush") as SolidColorBrush; + style.TryGetResource("Brush", out var brush); Assert.NotNull(brush); - Assert.Equal(Colors.White, brush.Color); + Assert.Equal(Colors.White, ((SolidColorBrush)brush).Color); - var d = (double)style.FindResource("Double"); + style.TryGetResource("Double", out var d); Assert.Equal(10.0, d); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 0c5a89b827..65fc9eaddd 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -141,7 +141,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var loader = new AvaloniaXamlLoader(); var window = (Window)loader.Load(xaml); - var brush = (ISolidColorBrush)window.FindStyleResource("brush"); + var brush = (ISolidColorBrush)window.FindResource("brush"); var button = window.FindControl