diff --git a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject index 235da29767..a8c3abe8f2 100644 --- a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject @@ -1,6 +1,7 @@  1000 + True True \ No newline at end of file diff --git a/.ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject b/.ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject new file mode 100644 index 0000000000..15d9efad87 --- /dev/null +++ b/.ncrunch/Avalonia.Markup.UnitTests.netcoreapp1.1.v3.ncrunchproject @@ -0,0 +1,9 @@ + + + + + Avalonia.Markup.UnitTests.Data.Plugins.DataAnnotationsValidationPluginTests.Produces_Aggregate_BindingNotificationsx + + + + \ No newline at end of file diff --git a/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject index 235da29767..a8c3abe8f2 100644 --- a/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Skia.RenderTests.v3.ncrunchproject @@ -1,6 +1,7 @@  1000 + True True \ No newline at end of file diff --git a/samples/ControlCatalog/MainWindow.xaml.cs b/samples/ControlCatalog/MainWindow.xaml.cs index c2e8c0c082..65c37c898b 100644 --- a/samples/ControlCatalog/MainWindow.xaml.cs +++ b/samples/ControlCatalog/MainWindow.xaml.cs @@ -20,7 +20,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", out _); AvaloniaXamlLoader.Load(this); } } diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 190b3ee2be..ec1643427b 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -47,6 +47,9 @@ namespace Avalonia { Dictionary inner; + // Ensure the type's static ctor has been run. + RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle); + if (_attached.TryGetValue(ownerType, out inner)) { return inner.Values; diff --git a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs index 1aa239180c..b90dccf74e 100644 --- a/src/Avalonia.Base/Collections/AvaloniaDictionary.cs +++ b/src/Avalonia.Base/Collections/AvaloniaDictionary.cs @@ -16,6 +16,7 @@ namespace Avalonia.Collections /// The type of the dictionary key. /// The type of the dictionary value. public class AvaloniaDictionary : IDictionary, + IDictionary, INotifyCollectionChanged, INotifyPropertyChanged { @@ -51,6 +52,16 @@ namespace Avalonia.Collections /// public ICollection Values => _inner.Values; + bool IDictionary.IsFixedSize => ((IDictionary)_inner).IsFixedSize; + + ICollection IDictionary.Keys => ((IDictionary)_inner).Keys; + + ICollection IDictionary.Values => ((IDictionary)_inner).Values; + + bool ICollection.IsSynchronized => ((IDictionary)_inner).IsSynchronized; + + object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; + /// /// Gets or sets the named resource. /// @@ -89,6 +100,8 @@ namespace Avalonia.Collections } } + object IDictionary.this[object key] { get => ((IDictionary)_inner)[key]; set => ((IDictionary)_inner)[key] = value; } + /// public void Add(TKey key, TValue value) { @@ -118,10 +131,7 @@ namespace Avalonia.Collections } /// - public bool ContainsKey(TKey key) - { - return _inner.ContainsKey(key); - } + public bool ContainsKey(TKey key) => _inner.ContainsKey(key); /// public void CopyTo(KeyValuePair[] array, int arrayIndex) @@ -130,21 +140,16 @@ namespace Avalonia.Collections } /// - public IEnumerator> GetEnumerator() - { - return _inner.GetEnumerator(); - } + public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); /// public bool Remove(TKey key) { - TValue value; - - if (_inner.TryGetValue(key, out value)) + if (_inner.TryGetValue(key, out TValue value)) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs($"Item[{key}]")); - + if (CollectionChanged != null) { var e = new NotifyCollectionChangedEventArgs( @@ -163,16 +168,13 @@ namespace Avalonia.Collections } /// - public bool TryGetValue(TKey key, out TValue value) - { - return _inner.TryGetValue(key, out value); - } + public bool TryGetValue(TKey key, out TValue value) => _inner.TryGetValue(key, out value); /// - IEnumerator IEnumerable.GetEnumerator() - { - return _inner.GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); + + /// + void ICollection.CopyTo(Array array, int index) => ((ICollection)_inner).CopyTo(array, index); /// void ICollection>.Add(KeyValuePair item) @@ -192,6 +194,18 @@ namespace Avalonia.Collections return Remove(item.Key); } + /// + void IDictionary.Add(object key, object value) => Add((TKey)key, (TValue)value); + + /// + bool IDictionary.Contains(object key) => ((IDictionary) _inner).Contains(key); + + /// + IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator(); + + /// + void IDictionary.Remove(object key) => Remove((TKey)key); + private void NotifyAdd(TKey key, TValue value) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Count")); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index 18eb1f7606..abae080515 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, IResourceNode { /// /// The application-global data templates. @@ -40,6 +40,7 @@ namespace Avalonia new Lazy(() => (IClipboard)AvaloniaLocator.Current.GetService(typeof(IClipboard))); private readonly Styler _styler = new Styler(); private Styles _styles; + private IResourceDictionary _resources; /// /// Initializes a new instance of the class. @@ -49,6 +50,9 @@ namespace Avalonia OnExit += OnExiting; } + /// + public event EventHandler ResourcesChanged; + /// /// Gets the current instance of the class. /// @@ -97,6 +101,34 @@ namespace Avalonia /// public IClipboard Clipboard => _clipboard.Value; + /// + /// Gets the application's global resource dictionary. + /// + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) + { + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ResourcesChanged; + } + + _resources = value; + _resources.ResourcesChanged += ResourcesChanged; + + if (hadResources || _resources.Count > 0) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + } + } + /// /// Gets the application's global styles. /// @@ -119,6 +151,12 @@ namespace Avalonia /// bool IStyleHost.IsStylesInitialized => _styles != null; + /// + bool IResourceProvider.HasResources => _resources?.Count > 0; + + /// + IResourceNode IResourceNode.ResourceParent => null; + /// /// Initializes the application by loading XAML etc. /// @@ -145,13 +183,20 @@ namespace Avalonia { OnExit?.Invoke(this, EventArgs.Empty); } - + + /// + bool IResourceProvider.TryGetResource(string key, out object value) + { + value = null; + return (_resources?.TryGetResource(key, out value) ?? false) || + 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 4913037ea4..f3705c9127 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -97,8 +97,9 @@ namespace Avalonia.Controls private bool _isAttachedToLogicalTree; private IAvaloniaList _logicalChildren; private INameScope _nameScope; - private bool _styled; + private IResourceDictionary _resources; private Styles _styles; + private bool _styled; private Subject _styleDetach = new Subject(); /// @@ -153,6 +154,11 @@ namespace Avalonia.Controls /// public event EventHandler Initialized; + /// + /// Occurs when a resource in this control or a parent control has changed. + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets the name of the control. /// @@ -262,7 +268,32 @@ namespace Avalonia.Controls /// each control may in addition define its own styles which are applied to the control /// itself and its children. /// - public Styles Styles => _styles ?? (_styles = new Styles()); + public Styles Styles + { + get { return _styles ?? (Styles = new Styles()); } + set + { + Contract.Requires(value != null); + + if (_styles != value) + { + if (_styles != null) + { + (_styles as ISetStyleParent)?.SetParent(null); + _styles.ResourcesChanged -= ThisResourcesChanged; + } + + _styles = value; + + if (value is ISetStyleParent setParent && setParent.ResourceParent == null) + { + setParent.SetParent(this); + } + + _styles.ResourcesChanged += ThisResourcesChanged; + } + } + } /// /// Gets the control's logical parent. @@ -278,6 +309,34 @@ namespace Avalonia.Controls set { SetValue(ContextMenuProperty, value); } } + /// + /// Gets or sets the control's resource dictionary. + /// + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) + { + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ThisResourcesChanged; + } + + _resources = value; + _resources.ResourcesChanged += ThisResourcesChanged; + + if (hadResources || _resources.Count > 0) + { + ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + } + } + /// /// Gets or sets a user-defined object attached to the control. /// @@ -296,9 +355,35 @@ namespace Avalonia.Controls internal set { SetValue(TemplatedParentProperty, value); } } + /// + /// Gets the control's logical children. + /// + protected IAvaloniaList LogicalChildren + { + get + { + if (_logicalChildren == null) + { + var list = new AvaloniaList(); + list.ResetBehavior = ResetBehavior.Remove; + list.Validate = ValidateLogicalChild; + list.CollectionChanged += LogicalChildrenCollectionChanged; + _logicalChildren = list; + } + + return _logicalChildren; + } + } + /// bool IDataTemplateHost.IsDataTemplatesInitialized => _dataTemplates != null; + /// + /// Gets the collection in a form that allows adding and removing + /// pseudoclasses. + /// + protected IPseudoClasses PseudoClasses => Classes; + /// /// Gets a value indicating whether the element is attached to a rooted logical tree. /// @@ -314,6 +399,12 @@ namespace Avalonia.Controls /// IAvaloniaReadOnlyList ILogical.LogicalChildren => LogicalChildren; + /// + bool IResourceProvider.HasResources => _resources?.Count > 0 || Styles.HasResources; + + /// + IResourceNode IResourceNode.ResourceParent => ((IStyleHost)this).StylingParent as IResourceNode; + /// IAvaloniaReadOnlyList IStyleable.Classes => Classes; @@ -390,31 +481,24 @@ namespace Avalonia.Controls this.OnDetachedFromLogicalTreeCore(e); } - /// - /// Gets the control's logical children. - /// - protected IAvaloniaList LogicalChildren + /// + void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) { - get - { - if (_logicalChildren == null) - { - var list = new AvaloniaList(); - list.ResetBehavior = ResetBehavior.Remove; - list.Validate = ValidateLogicalChild; - list.CollectionChanged += LogicalChildrenCollectionChanged; - _logicalChildren = list; - } + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - return _logicalChildren; + foreach (var child in LogicalChildren) + { + child.NotifyResourcesChanged(e); } } - /// - /// Gets the collection in a form that allows adding and removing - /// pseudoclasses. - /// - protected IPseudoClasses PseudoClasses => Classes; + /// + bool IResourceProvider.TryGetResource(string key, out object value) + { + value = null; + return (_resources?.TryGetResource(key, out value) ?? false) || + (_styles?.TryGetResource(key, out value) ?? false); + } /// /// Sets the control's logical parent. @@ -450,6 +534,7 @@ namespace Avalonia.Controls } _parent = (IControl)parent; + ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) { @@ -841,5 +926,10 @@ namespace Avalonia.Controls } } } + + private void ThisResourcesChanged(object sender, ResourcesChangedEventArgs e) + { + ((ILogical)this).NotifyResourcesChanged(e); + } } } diff --git a/src/Avalonia.Controls/IControl.cs b/src/Avalonia.Controls/IControl.cs index 41436c6d87..36e09b2ea1 100644 --- a/src/Avalonia.Controls/IControl.cs +++ b/src/Avalonia.Controls/IControl.cs @@ -20,6 +20,7 @@ namespace Avalonia.Controls ILayoutable, IInputElement, INamed, + IResourceNode, IStyleable, IStyleHost { 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.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index f8db0e2a5b..1af347ab4e 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -10,9 +10,11 @@ using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Layout; using Avalonia.Logging; +using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.Styling; +using Avalonia.Utilities; using Avalonia.VisualTree; using JetBrains.Annotations; @@ -26,7 +28,13 @@ namespace Avalonia.Controls /// It handles scheduling layout, styling and rendering as well as /// tracking the widget's . /// - public abstract class TopLevel : ContentControl, IInputRoot, ILayoutRoot, IRenderRoot, ICloseable, IStyleRoot + public abstract class TopLevel : ContentControl, + IInputRoot, + ILayoutRoot, + IRenderRoot, + ICloseable, + IStyleRoot, + IWeakSubscriber { /// /// Defines the property. @@ -100,7 +108,6 @@ namespace Avalonia.Controls impl.Resized = HandleResized; impl.ScalingChanged = HandleScalingChanged; - _keyboardNavigationHandler?.SetOwner(this); _accessKeyHandler?.SetOwner(this); styler?.ApplyStyles(this); @@ -116,6 +123,14 @@ namespace Avalonia.Controls { _applicationLifecycle.OnExit += OnApplicationExiting; } + + if (((IStyleHost)this).StylingParent is IResourceProvider applicationResources) + { + WeakSubscriptionManager.Subscribe( + applicationResources, + nameof(IResourceProvider.ResourcesChanged), + this); + } } /// @@ -165,6 +180,11 @@ namespace Avalonia.Controls /// IMouseDevice IInputRoot.MouseDevice => PlatformImpl?.MouseDevice; + void IWeakSubscriber.OnEvent(object sender, ResourcesChangedEventArgs e) + { + ((ILogical)this).NotifyResourcesChanged(e); + } + /// /// Gets or sets a value indicating whether access keys are shown in the window. /// diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index 965b6d87e6..6c0b957bf4 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -2,6 +2,7 @@ netstandard2.0 false + Avalonia true diff --git a/src/Avalonia.Styling/Controls/IResourceDictionary.cs b/src/Avalonia.Styling/Controls/IResourceDictionary.cs new file mode 100644 index 0000000000..d7c86b7d74 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceDictionary.cs @@ -0,0 +1,19 @@ +// 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 +{ + /// + /// An indexed dictionary of resources. + /// + public interface IResourceDictionary : IResourceProvider, IDictionary + { + /// + /// Gets a collection of child resource dictionaries. + /// + IList MergedDictionaries { get; } + } +} diff --git a/src/Avalonia.Styling/Controls/IResourceNode.cs b/src/Avalonia.Styling/Controls/IResourceNode.cs new file mode 100644 index 0000000000..b6a6cdc3e3 --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceNode.cs @@ -0,0 +1,15 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Represents resource provider in a tree. + /// + public interface IResourceNode : IResourceProvider + { + /// + /// Gets the parent resource node, if any. + /// + IResourceNode ResourceParent { get; } + } +} diff --git a/src/Avalonia.Styling/Controls/IResourceProvider.cs b/src/Avalonia.Styling/Controls/IResourceProvider.cs new file mode 100644 index 0000000000..eec783623c --- /dev/null +++ b/src/Avalonia.Styling/Controls/IResourceProvider.cs @@ -0,0 +1,33 @@ +using System; + +namespace Avalonia.Controls +{ + /// + /// Represents an object that can be queried for resources. + /// + public interface IResourceProvider + { + /// + /// Raised when resources in the provider are changed. + /// + event EventHandler ResourcesChanged; + + /// + /// Gets a value indicating whether the element has resources. + /// + bool HasResources { get; } + + /// + /// Tries to find a resource within the provider. + /// + /// 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 new file mode 100644 index 0000000000..74a861b36b --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourceDictionary.cs @@ -0,0 +1,101 @@ +// 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; +using System.Collections.Specialized; +using System.Linq; +using Avalonia.Collections; + +namespace Avalonia.Controls +{ + /// + /// An indexed dictionary of resources. + /// + public class ResourceDictionary : AvaloniaDictionary, IResourceDictionary + { + private AvaloniaList _mergedDictionaries; + + /// + /// Initializes a new instance of the class. + /// + public ResourceDictionary() + { + CollectionChanged += OnCollectionChanged; + } + + /// + public event EventHandler ResourcesChanged; + + /// + public IList MergedDictionaries + { + get + { + if (_mergedDictionaries == null) + { + _mergedDictionaries = new AvaloniaList(); + _mergedDictionaries.ResetBehavior = ResetBehavior.Remove; + _mergedDictionaries.ForEachItem( + x => + { + if (x.HasResources) + { + OnResourcesChanged(); + } + + x.ResourcesChanged += MergedDictionaryResourcesChanged; + }, + x => + { + if (x.HasResources) + { + OnResourcesChanged(); + } + + x.ResourcesChanged -= MergedDictionaryResourcesChanged; + }, + () => { }); + } + + return _mergedDictionaries; + } + } + + /// + bool IResourceProvider.HasResources + { + get => Count > 0 || (_mergedDictionaries?.Any(x => x.HasResources) ?? false); + } + + /// + public bool TryGetResource(string key, out object value) + { + if (TryGetValue(key, out value)) + { + return true; + } + + if (_mergedDictionaries != null) + { + for (var i = _mergedDictionaries.Count - 1; i >= 0; --i) + { + if (_mergedDictionaries[i].TryGetResource(key, out value)) + { + return true; + } + } + } + + return false; + } + + private void OnResourcesChanged() + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) => OnResourcesChanged(); + private void MergedDictionaryResourcesChanged(object sender, ResourcesChangedEventArgs e) => OnResourcesChanged(); + } +} diff --git a/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs new file mode 100644 index 0000000000..1f25fa132d --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourceProviderExtensions.cs @@ -0,0 +1,65 @@ +using System; +using System.Reactive; +using System.Reactive.Linq; + +namespace Avalonia.Controls +{ + public static class ResourceProviderExtensions + { + /// + /// Finds the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// The resource, or if not found. + public static object FindResource(this IResourceNode control, string key) + { + if (control.TryFindResource(key, out var value)) + { + return value; + } + + return AvaloniaProperty.UnsetValue; + } + + /// + /// Tries to the specified resource by searching up the logical tree and then global styles. + /// + /// The control. + /// The resource key. + /// On return, contains the resource if found, otherwise null. + /// True if the resource was found; otherwise false. + public static bool TryFindResource(this IResourceNode control, string key, out object value) + { + Contract.Requires(control != null); + Contract.Requires(key != null); + + var current = control; + + while (current != null) + { + if (current is IResourceNode host) + { + if (host.TryGetResource(key, out value)) + { + return true; + } + } + + current = current.ResourceParent; + } + + value = null; + return false; + } + + public static IObservable GetResourceObservable(this IResourceNode target, string key) + { + return Observable.FromEventPattern( + x => target.ResourcesChanged += x, + x => target.ResourcesChanged -= x) + .StartWith((EventPattern)null) + .Select(x => target.FindResource(key)); + } + } +} diff --git a/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs new file mode 100644 index 0000000000..68c7a58ab9 --- /dev/null +++ b/src/Avalonia.Styling/Controls/ResourcesChangedEventArgs.cs @@ -0,0 +1,11 @@ +// 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.Controls +{ + public class ResourcesChangedEventArgs : EventArgs + { + } +} diff --git a/src/Avalonia.Styling/LogicalTree/ILogical.cs b/src/Avalonia.Styling/LogicalTree/ILogical.cs index 006a9f5cc1..8ee3c9ea4f 100644 --- a/src/Avalonia.Styling/LogicalTree/ILogical.cs +++ b/src/Avalonia.Styling/LogicalTree/ILogical.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Collections; +using Avalonia.Controls; namespace Avalonia.LogicalTree { @@ -55,5 +56,15 @@ namespace Avalonia.LogicalTree /// this method yourself. /// void NotifyDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e); + + /// + /// Notifies the control that a change has been made to resources that apply to it. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. + /// + void NotifyResourcesChanged(ResourcesChangedEventArgs e); } } diff --git a/src/Avalonia.Styling/Properties/AssemblyInfo.cs b/src/Avalonia.Styling/Properties/AssemblyInfo.cs index b53681aeed..0a639139f7 100644 --- a/src/Avalonia.Styling/Properties/AssemblyInfo.cs +++ b/src/Avalonia.Styling/Properties/AssemblyInfo.cs @@ -6,5 +6,7 @@ using System.Runtime.CompilerServices; using Avalonia.Metadata; [assembly: AssemblyTitle("Avalonia.Styling")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")] +[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")] [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")] [assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests")] \ No newline at end of file diff --git a/src/Avalonia.Styling/Styling/ISetStyleParent.cs b/src/Avalonia.Styling/Styling/ISetStyleParent.cs new file mode 100644 index 0000000000..9f5855b401 --- /dev/null +++ b/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 +{ + /// + /// Defines an interface through which a 's parent can be set. + /// + /// + /// You should not usually need to use this interface - it is for internal use only. + /// + public interface ISetStyleParent : IStyle + { + /// + /// Sets the style parent. + /// + /// The parent. + void SetParent(IResourceNode parent); + + /// + /// Notifies the style that a change has been made to resources that apply to it. + /// + /// The event args. + /// + /// This method will be called automatically by the framework, you should not need to call + /// this method yourself. + /// + void NotifyResourcesChanged(ResourcesChangedEventArgs e); + } +} diff --git a/src/Avalonia.Styling/Styling/IStyle.cs b/src/Avalonia.Styling/Styling/IStyle.cs index 7b8510fe2d..5f12763825 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 : IResourceNode { /// /// 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/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 3dfd9118af..4182ffada3 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Reactive.Linq; +using Avalonia.Controls; using Avalonia.Metadata; namespace Avalonia.Styling @@ -11,12 +13,12 @@ namespace Avalonia.Styling /// /// Defines a style. /// - public class Style : IStyle + public class Style : IStyle, ISetStyleParent { private static Dictionary> _applied = new Dictionary>(); - - private StyleResources _resources; + private IResourceNode _parent; + private IResourceDictionary _resources; /// /// Initializes a new instance of the class. @@ -34,33 +36,33 @@ namespace Avalonia.Styling Selector = selector(null); } + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets a dictionary of style resources. /// - public StyleResources Resources + public IResourceDictionary Resources { - get + get => _resources ?? (Resources = new ResourceDictionary()); + set { - if (_resources == null) + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) { - _resources = new StyleResources(); + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ResourceDictionaryChanged; } - return _resources; - } + _resources = value; + _resources.ResourcesChanged += ResourceDictionaryChanged; - set - { - - var resources = Resources; - if (!Equals(resources, value)) + if (hadResources || _resources.Count > 0) { - foreach (var i in value) - { - resources[i.Key] = i.Value; - //resources.Add(i.Key, i.Value); - //(resources as IDictionary).Add(i); - } + ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); } } } @@ -76,6 +78,12 @@ namespace Avalonia.Styling [Content] public IList Setters { get; set; } = new List(); + /// + IResourceNode IResourceNode.ResourceParent => _parent; + + /// + bool IResourceProvider.HasResources => _resources?.Count > 0; + /// /// Attaches the style to a control if the style's selector matches. /// @@ -112,25 +120,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; } /// @@ -149,6 +143,23 @@ namespace Avalonia.Styling } } + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + + /// + void ISetStyleParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; + } + private static List GetSubscriptions(IStyleable control) { List subscriptions; @@ -179,5 +190,10 @@ namespace Avalonia.Styling _applied.Remove(control); } + + private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } } } diff --git a/src/Avalonia.Styling/Styling/StyleExtensions.cs b/src/Avalonia.Styling/Styling/StyleExtensions.cs deleted file mode 100644 index d53d00aed3..0000000000 --- a/src/Avalonia.Styling/Styling/StyleExtensions.cs +++ /dev/null @@ -1,42 +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) - { - if (control.IsStylesInitialized) - { - 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/StyleResources.cs b/src/Avalonia.Styling/Styling/StyleResources.cs deleted file mode 100644 index e447c6adfd..0000000000 --- a/src/Avalonia.Styling/Styling/StyleResources.cs +++ /dev/null @@ -1,90 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; - -namespace Avalonia.Styling -{ - /// - /// Holds resources for a . - /// - public class StyleResources : IDictionary, IDictionary - { - private Dictionary _inner = new Dictionary(); - - public object this[string key] - { - get { return _inner[key]; } - set { _inner[key] = value; } - } - - public int Count => _inner.Count; - - ICollection IDictionary.Keys => _inner.Keys; - - ICollection IDictionary.Values => _inner.Values; - - bool ICollection>.IsReadOnly => false; - - object IDictionary.this[object key] - { - get { return ((IDictionary)_inner)[key]; } - set { ((IDictionary)_inner)[key] = value; } - } - - ICollection IDictionary.Keys => _inner.Keys; - - ICollection IDictionary.Values => _inner.Values; - - bool ICollection.IsSynchronized => false; - - object ICollection.SyncRoot => ((IDictionary)_inner).SyncRoot; - - bool IDictionary.IsFixedSize => false; - - bool IDictionary.IsReadOnly => false; - - public void Add(string key, object value) => _inner.Add(key, value); - - public void Clear() => _inner.Clear(); - - public bool ContainsKey(string key) => _inner.ContainsKey(key); - - public bool Remove(string key) => _inner.Remove(key); - - public IEnumerator> GetEnumerator() => _inner.GetEnumerator(); - - public bool TryGetValue(string key, out object value) => _inner.TryGetValue(key, out value); - - bool ICollection>.Contains(KeyValuePair item) - { - return ((IDictionary)_inner).Contains(item); - } - - void ICollection>.Add(KeyValuePair item) - { - ((IDictionary)_inner).Add(item); - } - - void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) - { - ((IDictionary)_inner).CopyTo(array, arrayIndex); - } - - bool ICollection>.Remove(KeyValuePair item) - { - return ((IDictionary)_inner).Remove(item); - } - - void ICollection.CopyTo(Array array, int index) => ((IDictionary)_inner).CopyTo(array, index); - - IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator(); - - IDictionaryEnumerator IDictionary.GetEnumerator() => ((IDictionary)_inner).GetEnumerator(); - - void IDictionary.Add(object key, object value) => ((IDictionary)_inner).Add(key, value); - - bool IDictionary.Contains(object key) => ((IDictionary)_inner).Contains(key); - - void IDictionary.Remove(object key) => ((IDictionary)_inner).Remove(key); - } -} diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 770ef8344f..714e7f6def 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -1,16 +1,95 @@ // 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.Linq; using Avalonia.Collections; +using Avalonia.Controls; namespace Avalonia.Styling { /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaList, IStyle + public class Styles : AvaloniaList, IStyle, ISetStyleParent { + private IResourceNode _parent; + private IResourceDictionary _resources; + + public Styles() + { + ResetBehavior = ResetBehavior.Remove; + 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()); + } + + x.ResourcesChanged += SubResourceChanged; + }, + x => + { + if (x.ResourceParent == this && x is ISetStyleParent setParent) + { + setParent.SetParent(null); + setParent.NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + + if (x.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + + x.ResourcesChanged -= SubResourceChanged; + }, + () => { }); + } + + /// + public event EventHandler ResourcesChanged; + + /// + public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); + + /// + /// Gets or sets a dictionary of style resources. + /// + public IResourceDictionary Resources + { + get => _resources ?? (Resources = new ResourceDictionary()); + set + { + Contract.Requires(value != null); + + var hadResources = false; + + if (_resources != null) + { + hadResources = _resources.Count > 0; + _resources.ResourcesChanged -= ResourceDictionaryChanged; + } + + _resources = value; + _resources.ResourcesChanged += ResourceDictionaryChanged; + + if (hadResources || _resources.Count > 0) + { + ((ISetStyleParent)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); + } + } + } + + /// + IResourceNode IResourceNode.ResourceParent => _parent; + /// /// Attaches the style to a control if the style's selector matches. /// @@ -26,26 +105,68 @@ 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) + { + if (_resources != null && _resources.TryGetValue(key, out value)) + { + return true; + } + + for (var i = Count - 1; i >= 0; --i) + { + if (this[i].TryGetResource(key, out value)) + { + return true; + } + } + + value = null; + return false; + } + + /// + void ISetStyleParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; + } + + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) + { + ResourcesChanged?.Invoke(this, e); + } + + private void ResourceDictionaryChanged(object sender, ResourcesChangedEventArgs e) { - foreach (var style in this.Reverse()) + foreach (var child in this) { - var result = style.FindResource(name); + (child as ISetStyleParent)?.NotifyResourcesChanged(e); + } + + ResourcesChanged?.Invoke(this, e); + } - if (result != AvaloniaProperty.UnsetValue) + private void SubResourceChanged(object sender, ResourcesChangedEventArgs e) + { + var foundSource = false; + + foreach (var child in this) + { + if (foundSource) { - return result; + (child as ISetStyleParent)?.NotifyResourcesChanged(e); } + + foundSource |= child == sender; } - return AvaloniaProperty.UnsetValue; + ResourcesChanged?.Invoke(this, e); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 99b62181d7..de015fc947 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -34,6 +34,9 @@ + + + diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs index 083cb24b11..5ec40fe702 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/DelayedBinding.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Generic; +using System.Reflection; using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Data; +using Avalonia.Logging; namespace Avalonia.Markup.Xaml.Data { @@ -50,7 +52,36 @@ namespace Avalonia.Markup.Xaml.Data target.Initialized += ApplyBindings; } - bindings.Add(new Entry(binding, property)); + bindings.Add(new BindingEntry(property, binding)); + } + } + + /// + /// Adds a delayed value to a control. + /// + /// The control. + /// The property on the control to bind to. + /// A function which returns the value. + public static void Add(IControl target, PropertyInfo property, Func value) + { + if (target.IsInitialized) + { + property.SetValue(target, value(target)); + } + else + { + List bindings; + + if (!_entries.TryGetValue(target, out bindings)) + { + bindings = new List(); + _entries.Add(target, bindings); + + // TODO: Make this a weak event listener. + target.Initialized += ApplyBindings; + } + + bindings.Add(new ClrPropertyValueEntry(property, value)); } } @@ -60,13 +91,13 @@ namespace Avalonia.Markup.Xaml.Data /// The control. public static void ApplyBindings(IControl control) { - List bindings; + List entries; - if (_entries.TryGetValue(control, out bindings)) + if (_entries.TryGetValue(control, out entries)) { - foreach (var binding in bindings) + foreach (var entry in entries) { - control.Bind(binding.Property, binding.Binding); + entry.Apply(control); } _entries.Remove(control); @@ -80,9 +111,14 @@ namespace Avalonia.Markup.Xaml.Data target.Initialized -= ApplyBindings; } - private class Entry + private abstract class Entry + { + public abstract void Apply(IControl control); + } + + private class BindingEntry : Entry { - public Entry(IBinding binding, AvaloniaProperty property) + public BindingEntry(AvaloniaProperty property, IBinding binding) { Binding = binding; Property = property; @@ -90,6 +126,41 @@ namespace Avalonia.Markup.Xaml.Data public IBinding Binding { get; } public AvaloniaProperty Property { get; } + + public override void Apply(IControl control) + { + control.Bind(Property, Binding); + } + } + + private class ClrPropertyValueEntry : Entry + { + public ClrPropertyValueEntry(PropertyInfo property, Func value) + { + Property = property; + Value = value; + } + + public PropertyInfo Property { get; } + public Func Value { get; } + + public override void Apply(IControl control) + { + try + { + Property.SetValue(control, Value(control)); + } + catch (Exception e) + { + Logger.Error( + LogArea.Property, + control, + "Error setting {Property} on {Target}: {Exception}", + Property.Name, + control, + e); + } + } } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs new file mode 100644 index 0000000000..035765fae0 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Data/ResourceInclude.cs @@ -0,0 +1,63 @@ +using System; +using System.ComponentModel; +using Avalonia.Controls; +using Portable.Xaml.ComponentModel; +using Portable.Xaml.Markup; + +namespace Avalonia.Markup.Xaml.Data +{ + /// + /// Loads a resource dictionary from a specified URL. + /// + public class ResourceInclude : MarkupExtension, IResourceProvider + { + private Uri _baseUri; + private IResourceDictionary _loaded; + + public event EventHandler ResourcesChanged; + + /// + /// Gets the loaded resource dictionary. + /// + public IResourceDictionary Loaded + { + get + { + if (_loaded == null) + { + var loader = new AvaloniaXamlLoader(); + _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + + if (_loaded.HasResources) + { + ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); + } + } + + return _loaded; + } + } + + /// + /// Gets or sets the source URL. + /// + public Uri Source { get; set; } + + /// + bool IResourceProvider.HasResources => Loaded.HasResources; + + /// + bool IResourceProvider.TryGetResource(string key, out object value) + { + return Loaded.TryGetResource(key, out value); + } + + /// + public override object ProvideValue(IServiceProvider serviceProvider) + { + var tdc = (ITypeDescriptorContext)serviceProvider; + _baseUri = tdc?.GetBaseUri(); + return this; + } + } +} 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/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs new file mode 100644 index 0000000000..231778be09 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -0,0 +1,71 @@ +// 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.ComponentModel; +using System.Linq; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Data; +using Portable.Xaml; +using Portable.Xaml.ComponentModel; +using Portable.Xaml.Markup; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class DynamicResourceExtension : MarkupExtension, IBinding + { + private IResourceNode _anchor; + + public DynamicResourceExtension() + { + } + + public DynamicResourceExtension(string resourceKey) + { + ResourceKey = resourceKey; + } + + public string ResourceKey { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + var context = (ITypeDescriptorContext)serviceProvider; + var provideTarget = context.GetService(); + + if (!(provideTarget.TargetObject is IResourceNode)) + { + _anchor = GetAnchor(context); + } + + return this; + } + + InstancedBinding IBinding.Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor, + bool enableDataValidation) + { + var control = target as IResourceNode ?? _anchor; + + if (control != null) + { + return new InstancedBinding(control.GetResourceObservable(ResourceKey)); + } + + return null; + } + + private T GetAnchor(ITypeDescriptorContext context) where T : class + { + var schemaContext = context.GetService().SchemaContext; + var ambientProvider = context.GetService(); + var xamlType = schemaContext.GetXamlType(typeof(T)); + + // We override XamlType.CanAssignTo in BindingXamlType so the results we get back + // from GetAllAmbientValues aren't necessarily of the correct type. + return ambientProvider.GetAllAmbientValues(xamlType).OfType().FirstOrDefault(); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs new file mode 100644 index 0000000000..9089a13656 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StaticResourceExtension.cs @@ -0,0 +1,84 @@ +// 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; +using System.ComponentModel; +using System.Reflection; +using Avalonia.Controls; +using Avalonia.Markup.Xaml.Data; +using Portable.Xaml; +using Portable.Xaml.ComponentModel; +using Portable.Xaml.Markup; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class StaticResourceExtension : MarkupExtension + { + public StaticResourceExtension() + { + } + + public StaticResourceExtension(string resourceKey) + { + ResourceKey = resourceKey; + } + + public string ResourceKey { get; set; } + + public override object ProvideValue(IServiceProvider serviceProvider) + { + var context = (ITypeDescriptorContext)serviceProvider; + var schemaContext = context.GetService().SchemaContext; + var ambientProvider = context.GetService(); + var resourceProviderType = schemaContext.GetXamlType(typeof(IResourceNode)); + var ambientValues = ambientProvider.GetAllAmbientValues(resourceProviderType); + + // Look upwards though the ambient context for IResourceProviders which might be able + // to give us the resource. + // + // TODO: If we're in a template then only the ambient values since the root of the + // template wil be included here. We need some way to get hold of the parent ambient + // context and search that. See the test: + // + // StaticResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File + // + foreach (var ambientValue in ambientValues) + { + // We override XamlType.CanAssignTo in BindingXamlType so the results we get back + // from GetAllAmbientValues aren't necessarily of the correct type. + if (ambientValue is IResourceNode resourceProvider) + { + if (resourceProvider is IControl control && control.StylingParent != null) + { + // If we've got to a control that has a StylingParent then it's probably + // a top level control and its StylingParent is pointing to the global + // styles. If this is case just do a FindResource on it. + return control.FindResource(ResourceKey); + } + else if (resourceProvider.TryGetResource(ResourceKey, out var value)) + { + return value; + } + } + } + + // The resource still hasn't been found, so add a delayed one-time binding. + var provideTarget = context.GetService(); + + if (provideTarget.TargetObject is IControl target && + provideTarget.TargetProperty is PropertyInfo property) + { + DelayedBinding.Add(target, property, GetValue); + return AvaloniaProperty.UnsetValue; + } + + throw new KeyNotFoundException($"Static resource '{ResourceKey}' not found."); + } + + private object GetValue(IControl control) + { + return control.FindResource(ResourceKey); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index 0f4824d493..fb308e62b8 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -3,27 +3,31 @@ using Avalonia.Styling; using System; +using Avalonia.Controls; namespace Avalonia.Markup.Xaml.Styling { /// /// Includes a style from a URL. /// - public class StyleInclude : IStyle + public class StyleInclude : IStyle, ISetStyleParent { private Uri _baseUri; private IStyle _loaded; + private IResourceNode _parent; /// /// Initializes a new instance of the class. /// /// - public StyleInclude(Uri baseUri) { _baseUri = baseUri; } + /// + public event EventHandler ResourcesChanged; + /// /// Gets or sets the source URL. /// @@ -40,12 +44,19 @@ namespace Avalonia.Markup.Xaml.Styling { var loader = new AvaloniaXamlLoader(); _loaded = (IStyle)loader.Load(Source, _baseUri); + (_loaded as ISetStyleParent)?.SetParent(this); } return _loaded; } } + /// + bool IResourceProvider.HasResources => Loaded.HasResources; + + /// + IResourceNode IResourceNode.ResourceParent => _parent; + /// public void Attach(IStyleable control, IStyleHost container) { @@ -55,16 +66,24 @@ 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) + /// + public bool TryGetResource(string key, out object value) => Loaded.TryGetResource(key, out value); + + /// + void ISetStyleParent.NotifyResourcesChanged(ResourcesChangedEventArgs e) { - return Loaded.FindResource(name); + (Loaded as ISetStyleParent)?.NotifyResourcesChanged(e); + } + + /// + void ISetStyleParent.SetParent(IResourceNode parent) + { + if (_parent != null && parent != null) + { + throw new InvalidOperationException("The Style already has a parent."); + } + + _parent = parent; } } } \ 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..6a6cb48001 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs @@ -0,0 +1,217 @@ +// 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.Presenters; +using Avalonia.Controls.Templates; +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" }, + } + } + }, + Resources = + { + { "bar", "bar-value" }, + }, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Styles_Resource() + { + var target = new Control + { + Styles = + { + new Styles + { + Resources = + { + { "foo", "foo-value" }, + } + } + }, + Resources = + { + { "bar", "bar-value" }, + }, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void FindResource_Should_Find_Application_Style_Resource() + { + Control target; + + var app = new Application + { + Styles = + { + new Style + { + Resources = + { + { "foo", "foo-value" }, + }, + } + }, + Resources = + { + { "bar", "bar-value" }, + }, + }; + + var root = new TestRoot + { + Child = target = new Control(), + StylingParent = app, + }; + + Assert.Equal("foo-value", target.FindResource("foo")); + } + + [Fact] + public void Adding_Resource_Should_Call_Raise_ResourceChanged_On_Logical_Children() + { + Border child; + + var target = new ContentControl + { + Content = child = new Border(), + Template = ContentControlTemplate(), + }; + + var raisedOnTarget = false; + var raisedOnPresenter = false; + var raisedOnChild = false; + + target.Measure(Size.Infinity); + target.ResourcesChanged += (_, __) => raisedOnTarget = true; + target.Presenter.ResourcesChanged += (_, __) => raisedOnPresenter = true; + child.ResourcesChanged += (_, __) => raisedOnChild = true; + + target.Resources.Add("foo", "bar"); + + Assert.True(raisedOnTarget); + Assert.False(raisedOnPresenter); + Assert.True(raisedOnChild); + } + + [Fact] + public void Adding_Resource_To_Styles_Should_Raise_ResourceChanged() + { + var target = new Decorator(); + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + target.Styles.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + + [Fact] + public void Adding_Resource_To_Nested_Style_Should_Raise_ResourceChanged() + { + Style style; + var target = new Decorator + { + Styles = + { + (style = new Style()), + } + }; + + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + style.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + + private IControlTemplate ContentControlTemplate() + { + return new FuncControlTemplate(x => + new ContentPresenter + { + Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty], + }); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index da30336be6..da0719893f 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -219,6 +219,23 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Adding_Resource_To_Application_Should_Raise_ResourcesChanged() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var impl = new Mock(); + impl.SetupAllProperties(); + var target = new TestTopLevel(impl.Object); + var raised = false; + + target.ResourcesChanged += (_, __) => raised = true; + Application.Current.Resources.Add("foo", "bar"); + + Assert.True(raised); + } + } + private FuncControlTemplate CreateTemplate() { return new FuncControlTemplate(x => diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index cdfb253bf4..3e7ff496a8 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/Data/ResourceIncludeTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs new file mode 100644 index 0000000000..6dc56e425c --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/ResourceIncludeTests.cs @@ -0,0 +1,55 @@ +using System; +using Avalonia.Controls; +using Avalonia.Media; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Data +{ + public class ResourceIncludeTests + { + public class StaticResourceExtensionTests + { + [Fact] + public void ResourceInclude_Loads_ResourceDictionary() + { + var includeXaml = @" + + #ff506070 + +"; + using (StartWithResources(("test:include.xaml", includeXaml))) + { + var xaml = @" + + + + + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + } + + private IDisposable StartWithResources(params (string, string)[] assets) + { + var assetLoader = new MockAssetLoader(assets); + var services = new TestServices(assetLoader: assetLoader); + return UnitTestApplication.Start(services); + } + } + } +} \ No newline at end of file diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs new file mode 100644 index 0000000000..d55e34cbe6 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs @@ -0,0 +1,660 @@ +// 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.Linq; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Templates; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Media; +using Avalonia.Styling; +using Avalonia.UnitTests; +using Avalonia.VisualTree; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions +{ + public class DynamicResourceExtensionTests + { + [Fact] + public void DynamicResource_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + #ff506070 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_Can_Be_Assigned_To_Attached_Property() + { + var xaml = @" + + + 5 + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + Assert.Equal(5, Grid.GetColumn(border)); + } + + [Fact] + public void DynamicResource_From_Style_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_From_MergedDictionary_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + + + + #ff506070 + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_From_MergedDictionary_In_Style_Can_Be_Assigned_To_Property() + { + var xaml = @" + + + + + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + DelayedBinding.ApplyBindings(border); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + + [Fact] + public void DynamicResource_From_Application_Can_Be_Assigned_To_Property_In_Window() + { + using (StyledWindow()) + { + Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070)); + + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var border = window.FindControl("border"); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + } + + [Fact] + public void DynamicResource_From_Application_Can_Be_Assigned_To_Property_In_UserControl() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + Application.Current.Resources.Add("brush", new SolidColorBrush(0xff506070)); + + var xaml = @" + + +"; + + var loader = new AvaloniaXamlLoader(); + var userControl = (UserControl)loader.Load(xaml); + var border = userControl.FindControl("border"); + + // We don't actually know where the global styles are until we attach the control + // to a window, as Window has StylingParent set to Application. + var window = new Window { Content = userControl }; + window.Show(); + + var brush = (SolidColorBrush)border.Background; + Assert.Equal(0xff506070, brush.Color.ToUint32()); + } + } + + [Fact] + public void DynamicResource_Can_Be_Assigned_To_Setter() + { + using (StyledWindow()) + { + var xaml = @" + + + #ff506070 + + + + +