diff --git a/appveyor.yml b/appveyor.yml index aa8c19ace4..76d1ae3e1c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -23,10 +23,6 @@ before_build: - git submodule update --init build_script: - ps: .\build.ps1 -Target "AppVeyor" -Platform "$env:platform" -Configuration "$env:configuration" -after_build: -- "SET PATH=C:\\Python34;C:\\Python34\\Scripts;%PATH%" -- pip install codecov -- codecov -f "./artifacts/coverage.xml" test: off artifacts: diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 685bf83a75..e96679e643 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -326,7 +326,7 @@ namespace Avalonia object anchor = null, bool enableDataValidation = false) { - return new InstancedBinding(_source); + return InstancedBinding.OneWay(_source); } } } diff --git a/src/Avalonia.Base/Data/IndexerBinding.cs b/src/Avalonia.Base/Data/IndexerBinding.cs index 729b21b0d9..6b5a84989a 100644 --- a/src/Avalonia.Base/Data/IndexerBinding.cs +++ b/src/Avalonia.Base/Data/IndexerBinding.cs @@ -31,13 +31,18 @@ namespace Avalonia.Data targetProperty.GetMetadata(target.GetType()).DefaultBindingMode : Mode; - if (mode == BindingMode.TwoWay) + switch (mode) { - return new InstancedBinding(Source.GetSubject(Property), mode); - } - else - { - return new InstancedBinding(Source.GetObservable(Property), mode); + case BindingMode.OneTime: + return InstancedBinding.OneTime(Source.GetObservable(Property)); + case BindingMode.OneWay: + return InstancedBinding.OneWay(Source.GetObservable(Property)); + case BindingMode.OneWayToSource: + return InstancedBinding.OneWayToSource(Source.GetSubject(Property)); + case BindingMode.TwoWay: + return InstancedBinding.TwoWay(Source.GetSubject(Property)); + default: + throw new NotSupportedException("Unsupported BindingMode."); } } } diff --git a/src/Avalonia.Base/Data/InstancedBinding.cs b/src/Avalonia.Base/Data/InstancedBinding.cs index 899cfed1fe..dc35fe076b 100644 --- a/src/Avalonia.Base/Data/InstancedBinding.cs +++ b/src/Avalonia.Base/Data/InstancedBinding.cs @@ -14,71 +14,35 @@ namespace Avalonia.Data /// property on a control's DataContext"; this class represents a binding that has been /// *instanced* by calling /// on a target object. - /// - /// When a binding is initiated, it can return one of 3 possible sources for the binding: - /// - An which can be used for any type of binding. - /// - An which can be used for all types of bindings except - /// and . - /// - A plain object, which can only represent a binding. /// public class InstancedBinding { /// /// Initializes a new instance of the class. /// - /// - /// The value used for the binding. - /// - /// The binding priority. - public InstancedBinding(object value, - BindingPriority priority = BindingPriority.LocalValue) - { - Mode = BindingMode.OneTime; - Priority = priority; - Value = value; - } - - /// - /// Initializes a new instance of the class. - /// - /// The observable for a one-way binding. + /// The binding source. /// The binding mode. - /// The binding priority. - public InstancedBinding( - IObservable observable, - BindingMode mode = BindingMode.OneWay, - BindingPriority priority = BindingPriority.LocalValue) + /// The priority of the binding. + /// + /// This constructor can be used to create any type of binding and as such requires an + /// as the binding source because this is the only binding + /// source which can be used for all binding modes. If you wish to create an instance with + /// something other than a subject, use one of the static creation methods on this class. + /// + public InstancedBinding(ISubject subject, BindingMode mode, BindingPriority priority) { - Contract.Requires(observable != null); - - if (mode == BindingMode.OneWayToSource || mode == BindingMode.TwoWay) - { - throw new ArgumentException( - "Invalid BindingResult mode: OneWayToSource and TwoWay bindings" + - "require a Subject."); - } + Contract.Requires(subject != null); Mode = mode; Priority = priority; - Observable = observable; + Value = subject; } - /// - /// Initializes a new instance of the class. - /// - /// The subject for a two-way binding. - /// The binding mode. - /// The binding priority. - public InstancedBinding( - ISubject subject, - BindingMode mode = BindingMode.OneWay, - BindingPriority priority = BindingPriority.LocalValue) + private InstancedBinding(object value, BindingMode mode, BindingPriority priority) { - Contract.Requires(subject != null); - Mode = mode; Priority = priority; - Subject = subject; + Value = value; } /// @@ -92,18 +56,101 @@ namespace Avalonia.Data public BindingPriority Priority { get; } /// - /// Gets the value used for a binding. + /// Gets the value or source of the binding. /// public object Value { get; } /// - /// Gets the observable for a one-way binding. + /// Gets the as an observable. /// - public IObservable Observable { get; } + public IObservable Observable => Value as IObservable; /// - /// Gets the subject for a two-way binding. + /// Gets the as a subject. /// - public ISubject Subject { get; } + public ISubject Subject => Value as ISubject; + + /// + /// Creates a new one-time binding with a fixed value. + /// + /// The value. + /// The priority of the binding. + /// An instance. + public static InstancedBinding OneTime( + object value, + BindingPriority priority = BindingPriority.LocalValue) + { + return new InstancedBinding(value, BindingMode.OneTime, priority); + } + + /// + /// Creates a new one-time binding. + /// + /// The source observable. + /// The priority of the binding. + /// An instance. + public static InstancedBinding OneTime( + IObservable observable, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(observable != null); + + return new InstancedBinding(observable, BindingMode.OneTime, priority); + } + + /// + /// Creates a new one-way binding. + /// + /// The source observable. + /// The priority of the binding. + /// An instance. + public static InstancedBinding OneWay( + IObservable observable, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(observable != null); + + return new InstancedBinding(observable, BindingMode.OneWay, priority); + } + + /// + /// Creates a new one-way to source binding. + /// + /// The binding source. + /// The priority of the binding. + /// An instance. + public static InstancedBinding OneWayToSource( + ISubject subject, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(subject != null); + + return new InstancedBinding(subject, BindingMode.OneWayToSource, priority); + } + + /// + /// Creates a new two-way binding. + /// + /// The binding source. + /// The priority of the binding. + /// An instance. + public static InstancedBinding TwoWay( + ISubject subject, + BindingPriority priority = BindingPriority.LocalValue) + { + Contract.Requires(subject != null); + + return new InstancedBinding(subject, BindingMode.TwoWay, priority); + } + + /// + /// Creates a copy of the with a different priority. + /// + /// The priority of the binding. + /// An instance. + public InstancedBinding WithPriority(BindingPriority priority) + { + return new InstancedBinding(Value, Mode, priority); + } } } diff --git a/src/Avalonia.Base/Threading/Dispatcher.cs b/src/Avalonia.Base/Threading/Dispatcher.cs index a4b1aaafbc..4a096fc326 100644 --- a/src/Avalonia.Base/Threading/Dispatcher.cs +++ b/src/Avalonia.Base/Threading/Dispatcher.cs @@ -72,6 +72,12 @@ namespace Avalonia.Threading _jobRunner?.RunJobs(null); } + /// + /// Use this method to ensure that more prioritized tasks are executed + /// + /// + public void RunJobs(DispatcherPriority minimumPriority) => _jobRunner.RunJobs(minimumPriority); + /// public Task InvokeTaskAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal) { diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index a7aafbf6e8..d4777b2f8a 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -774,7 +774,9 @@ namespace Avalonia.Controls foreach (var child in control.LogicalChildren) { - if (child is Control c && !c.IsSet(DataContextProperty)) + if (child is Control c && + c.InheritanceParent == control && + !c.IsSet(DataContextProperty)) { DataContextNotifying(c, notifying); } diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs index 04e6ece832..2e55dd3552 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs @@ -62,7 +62,7 @@ namespace Avalonia.Controls.Templates /// The child items, or null if no child items. public InstancedBinding ItemsSelector(object item) { - return new InstancedBinding(this?._itemsSelector(item)); + return InstancedBinding.OneTime(this?._itemsSelector(item)); } /// diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index eb5218cd6f..95db18a465 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -135,45 +135,47 @@ namespace Avalonia.Styling private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable activator) { - InstancedBinding cloned; - if (activator != null) { var description = style?.ToString(); - if (sourceInstance.Mode == BindingMode.TwoWay || sourceInstance.Mode == BindingMode.OneWayToSource) - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - cloned = new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); - } - else if (sourceInstance.Mode == BindingMode.OneTime) - { - var activated = new ActivatedValue(activator, sourceInstance.Value, description); - cloned = new InstancedBinding(activated, BindingMode.OneWay, BindingPriority.StyleTrigger); - } - else + switch (sourceInstance.Mode) { - var activated = new ActivatedObservable(activator, sourceInstance.Observable ?? sourceInstance.Subject, description); - cloned = new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); + case BindingMode.OneTime: + if (sourceInstance.Observable != null) + { + var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); + return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); + } + else + { + var activated = new ActivatedValue(activator, sourceInstance.Value, description); + return InstancedBinding.OneTime(activated, BindingPriority.StyleTrigger); + } + case BindingMode.OneWay: + { + var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); + return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); + } + case BindingMode.OneWayToSource: + { + var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); + return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger); + } + case BindingMode.TwoWay: + { + var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); + return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger); + } + default: + throw new NotSupportedException("Unsupported BindingMode."); } + } else { - if (sourceInstance.Subject != null) - { - cloned = new InstancedBinding(sourceInstance.Subject, sourceInstance.Mode, BindingPriority.Style); - } - else if (sourceInstance.Observable != null) - { - cloned = new InstancedBinding(sourceInstance.Observable, sourceInstance.Mode, BindingPriority.Style); - } - else - { - cloned = new InstancedBinding(sourceInstance.Value, BindingPriority.Style); - } + return sourceInstance.WithPriority(BindingPriority.StyleTrigger); } - - return cloned; } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index e07c22d750..a1db5c2915 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -39,7 +39,6 @@ - @@ -73,8 +72,6 @@ - - diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index 3f12593197..eb5f87ea2a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -83,6 +83,8 @@ namespace Avalonia.Markup.Xaml.Data /// public object Source { get; set; } + internal WeakReference DefaultAnchor { get; set; } + /// public InstancedBinding Initiate( IAvaloniaObject target, @@ -92,6 +94,8 @@ namespace Avalonia.Markup.Xaml.Data { Contract.Requires(target != null); + anchor = anchor ?? DefaultAnchor?.Target; + var pathInfo = ParsePath(Path); ValidateState(pathInfo); enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs index 621e06efba..3c29ed3a39 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/MultiBinding.cs @@ -62,41 +62,20 @@ namespace Avalonia.Markup.Xaml.Data } var targetType = targetProperty?.PropertyType ?? typeof(object); - var result = new BehaviorSubject(AvaloniaProperty.UnsetValue); var children = Bindings.Select(x => x.Initiate(target, null)); var input = children.Select(x => x.Subject).CombineLatest().Select(x => ConvertValue(x, targetType)); - input.Subscribe(result); - return new InstancedBinding(result, Mode, Priority); - } - - /// - /// Applies a binding subject to a property on an instance. - /// - /// The target instance. - /// The target property. - /// The binding subject. - internal void Bind(IAvaloniaObject target, AvaloniaProperty property, ISubject subject) - { var mode = Mode == BindingMode.Default ? - property.GetMetadata(target.GetType()).DefaultBindingMode : Mode; + targetProperty.GetMetadata(target.GetType()).DefaultBindingMode : Mode; switch (mode) { - case BindingMode.Default: - case BindingMode.OneWay: - target.Bind(property, subject, Priority); - break; - case BindingMode.TwoWay: - throw new NotSupportedException("TwoWay MultiBinding not currently supported."); case BindingMode.OneTime: - target.GetObservable(Control.DataContextProperty).Subscribe(dataContext => - { - subject.Take(1).Subscribe(x => target.SetValue(property, x, Priority)); - }); - break; - case BindingMode.OneWayToSource: - target.GetObservable(property).Subscribe(subject); - break; + return InstancedBinding.OneTime(input, Priority); + case BindingMode.OneWay: + return InstancedBinding.OneWay(input, Priority); + default: + throw new NotSupportedException( + "MultiBinding currently only supports OneTime and OneWay BindingMode."); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs deleted file mode 100644 index 787aebbdc6..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Data/StyleResourceBinding.cs +++ /dev/null @@ -1,69 +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; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Reactive.Subjects; -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Styling; - -namespace Avalonia.Markup.Xaml.Data -{ - public class StyleResourceBinding : IBinding - { - /// - /// Initializes a new instance of the class. - /// - /// The resource name. - public StyleResourceBinding(string name) - { - Name = name; - } - - /// - public BindingMode Mode => BindingMode.OneTime; - - /// - /// Gets the resource name. - /// - public string Name { get; } - - /// - public BindingPriority Priority => BindingPriority.LocalValue; - - /// - public InstancedBinding Initiate( - IAvaloniaObject target, - AvaloniaProperty targetProperty, - object anchor = null, - bool enableDataValidation = false) - { - var host = (target as IControl) ?? (anchor as IControl); - var style = anchor as IStyle; - var resource = AvaloniaProperty.UnsetValue; - - if (host != null) - { - resource = host.FindResource(Name); - } - else if (style != null) - { - if (!style.TryGetResource(Name, out resource)) - { - resource = AvaloniaProperty.UnsetValue; - } - } - - if (resource != AvaloniaProperty.UnsetValue) - { - return new InstancedBinding(resource, Priority); - } - else - { - return null; - } - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 9d1e36ee78..8984498393 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -7,8 +7,13 @@ using System; namespace Avalonia.Markup.Xaml.MarkupExtensions { + using Avalonia.Controls; + using Avalonia.Styling; + using Portable.Xaml; + using Portable.Xaml.ComponentModel; using Portable.Xaml.Markup; using PortableXaml; + using System.ComponentModel; [MarkupExtensionReturnType(typeof(IBinding))] public class BindingExtension : MarkupExtension @@ -24,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions public override object ProvideValue(IServiceProvider serviceProvider) { - var b = new Binding + return new Binding { Converter = Converter, ConverterParameter = ConverterParameter, @@ -33,10 +38,26 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Mode = Mode, Path = Path, Priority = Priority, - RelativeSource = RelativeSource + RelativeSource = RelativeSource, + DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider)) }; + } + + + private static object GetDefaultAnchor(ITypeDescriptorContext context) + { + object anchor = null; + + // The target is not a control, so we need to find an anchor that will let us look + // up named controls and style resources. First look for the closest IControl in + // the context. + anchor = context.GetFirstAmbientValue(); - return XamlBinding.FromMarkupExtensionContext(b, serviceProvider); + // If a control was not found, then try to find the highest-level style as the XAML + // file could be a XAML file containing only styles. + return anchor ?? + context.GetService()?.RootObject as IStyle ?? + context.GetLastOrDefaultAmbientValue(); } public IValueConverter Converter { get; set; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs index 231778be09..52d9f043f4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/DynamicResourceExtension.cs @@ -51,7 +51,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions if (control != null) { - return new InstancedBinding(control.GetResourceObservable(ResourceKey)); + return InstancedBinding.OneWay(control.GetResourceObservable(ResourceKey)); } return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleResourceExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleResourceExtension.cs deleted file mode 100644 index 55a1b72125..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/StyleResourceExtension.cs +++ /dev/null @@ -1,31 +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 Avalonia.Data; -using Avalonia.Markup.Xaml.Data; -using System; - -namespace Avalonia.Markup.Xaml.MarkupExtensions -{ - using Portable.Xaml.Markup; - using PortableXaml; - - [MarkupExtensionReturnType(typeof(IBinding))] - public class StyleResourceExtension : MarkupExtension - { - public StyleResourceExtension(string name) - { - Name = name; - } - - public override object ProvideValue(IServiceProvider serviceProvider) - { - return XamlBinding.FromMarkupExtensionContext( - new StyleResourceBinding(Name), - serviceProvider); - } - - [ConstructorArgument("name")] - public string Name { get; set; } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs index a38029964f..7de96ea220 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs @@ -212,11 +212,6 @@ namespace Avalonia.Markup.Xaml.PortableXaml value); } - if (value is XamlBinding) - { - value = (value as XamlBinding).Value; - } - if (UpdateListInsteadSet && value != null && UpdateListInsteadSetValue(instance, value)) @@ -317,9 +312,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml if (!Member.AssignBinding) ApplyBinding(obj, (IBinding)value); else - obj.SetValue(Property, value is XamlBinding ? - (value as XamlBinding).Value : - value); + obj.SetValue(Property, value); } else { @@ -348,12 +341,9 @@ namespace Avalonia.Markup.Xaml.PortableXaml { var control = obj as IControl; var property = Property; - var xamlBinding = binding as XamlBinding; if (control != null && property != Control.DataContextProperty) DelayedBinding.Add(control, property, binding); - else if (xamlBinding != null) - obj.Bind(property, xamlBinding.Value, xamlBinding.Anchor?.Target); else obj.Bind(property, binding); } diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/XamlBinding.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/XamlBinding.cs deleted file mode 100644 index 1f4e66471c..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/XamlBinding.cs +++ /dev/null @@ -1,63 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Data; -using Avalonia.Styling; -using Portable.Xaml; -using Portable.Xaml.ComponentModel; -using System.ComponentModel; -using Portable.Xaml.Markup; -using System; - -namespace Avalonia.Markup.Xaml.PortableXaml -{ - internal class XamlBinding : IBinding - { - public static IBinding FromMarkupExtensionContext( - IBinding binding, - IServiceProvider serviceProvider) - { - var context = (ITypeDescriptorContext)serviceProvider; - var pvt = context.GetService(); - - if (pvt.TargetObject is IControl) return binding; - - object anchor = GetDefaultAnchor(context); - - if (anchor == null) return binding; - - return new XamlBinding(binding, anchor); - } - - private static object GetDefaultAnchor(ITypeDescriptorContext context) - { - object anchor = null; - - // The target is not a control, so we need to find an anchor that will let us look - // up named controls and style resources. First look for the closest IControl in - // the context. - anchor = context.GetFirstAmbientValue(); - - // If a control was not found, then try to find the highest-level style as the XAML - // file could be a XAML file containing only styles. - return anchor ?? - context.GetService()?.RootObject as IStyle ?? - context.GetLastOrDefaultAmbientValue(); - } - - private XamlBinding(IBinding binding, object anchor) - { - Value = binding; - - Anchor = new WeakReference(anchor); - } - - public WeakReference Anchor { get; } - - public IBinding Value { get; } - - public InstancedBinding Initiate(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor = null, bool enableDataValidation = false) - { - return Value.Initiate(target, targetProperty, - anchor ?? Anchor.Target, enableDataValidation); - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index d6d3bd46ec..883e177f8a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -42,7 +42,7 @@ namespace Avalonia.Markup.Xaml.Templates if (ItemsSource != null) { var obs = new ExpressionObserver(item, ItemsSource.Path); - return new InstancedBinding(obs, BindingMode.OneWay, BindingPriority.Style); + return InstancedBinding.OneWay(obs, BindingPriority.Style); } return null; diff --git a/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs b/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs index 1ecfa0eb7d..935ab53432 100644 --- a/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs +++ b/src/OSX/Avalonia.MonoMac/EmulatedFramebuffer.cs @@ -2,44 +2,80 @@ using Avalonia.Platform; using MonoMac.AppKit; using System.Runtime.InteropServices; +using Avalonia.Threading; using MonoMac.CoreGraphics; namespace Avalonia.MonoMac { class EmulatedFramebuffer : ILockedFramebuffer { + private readonly TopLevelImpl.TopLevelView _view; private readonly CGSize _logicalSize; - public EmulatedFramebuffer(NSView view) + private readonly bool _isDeferred; + + [DllImport("libc")] + static extern void memset(IntPtr p, int c, IntPtr size); + + public EmulatedFramebuffer(TopLevelImpl.TopLevelView view) { - _logicalSize = view.Frame.Size; - var pixelSize = view.ConvertSizeToBacking(_logicalSize); + _view = view; + + _isDeferred = !Dispatcher.UIThread.CheckAccess(); + _logicalSize = _view.LogicalSize; + var pixelSize = _view.PixelSize; Width = (int)pixelSize.Width; Height = (int)pixelSize.Height; RowBytes = Width * 4; Dpi = new Vector(96 * pixelSize.Width / _logicalSize.Width, 96 * pixelSize.Height / _logicalSize.Height); Format = PixelFormat.Rgba8888; - Address = Marshal.AllocHGlobal(Height * RowBytes); + var size = Height * RowBytes; + Address = Marshal.AllocHGlobal(size); + memset(Address, 0, new IntPtr(size)); } - + public void Dispose() { if (Address == IntPtr.Zero) return; var nfo = (int) CGBitmapFlags.ByteOrder32Big | (int) CGImageAlphaInfo.PremultipliedLast; - - using (var colorSpace = CGColorSpace.CreateDeviceRGB()) - using (var bContext = new CGBitmapContext(Address, Width, Height, 8, Width * 4, - colorSpace, (CGImageAlphaInfo) nfo)) - using (var image = bContext.ToImage()) - using (var nscontext = NSGraphicsContext.CurrentContext) - using (var context = nscontext.GraphicsPort) + CGImage image = null; + try { - context.SetFillColor(255, 255, 255, 255); - context.FillRect(new CGRect(default(CGPoint), _logicalSize)); - context.DrawImage(new CGRect(default(CGPoint), _logicalSize), image); + using (var colorSpace = CGColorSpace.CreateDeviceRGB()) + using (var bContext = new CGBitmapContext(Address, Width, Height, 8, Width * 4, + colorSpace, (CGImageAlphaInfo)nfo)) + image = bContext.ToImage(); + lock (_view.SyncRoot) + { + if(!_isDeferred) + { + using (var nscontext = NSGraphicsContext.CurrentContext) + using (var context = nscontext.GraphicsPort) + { + context.SetFillColor(255, 255, 255, 255); + context.FillRect(new CGRect(default(CGPoint), _view.LogicalSize)); + context.TranslateCTM(0, _view.LogicalSize.Height - _logicalSize.Height); + context.DrawImage(new CGRect(default(CGPoint), _logicalSize), image); + context.Flush(); + nscontext.FlushGraphics(); + } + } + } } - Marshal.FreeHGlobal(Address); - Address = IntPtr.Zero; + finally + { + if (image != null) + { + if (!_isDeferred) + image.Dispose(); + else + _view.SetBackBufferImage(new SavedImage(image, _logicalSize)); + } + Marshal.FreeHGlobal(Address); + Address = IntPtr.Zero; + } + + } public IntPtr Address { get; private set; } @@ -49,4 +85,22 @@ namespace Avalonia.MonoMac public Vector Dpi { get; } public PixelFormat Format { get; } } + + class SavedImage : IDisposable + { + public CGImage Image { get; private set; } + public CGSize LogicalSize { get; } + + public SavedImage(CGImage image, CGSize logicalSize) + { + Image = image; + LogicalSize = logicalSize; + } + + public void Dispose() + { + Image?.Dispose(); + Image = null; + } + } } \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs index 0deea7fb44..a6b1f1d5b4 100644 --- a/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs +++ b/src/OSX/Avalonia.MonoMac/MonoMacPlatform.cs @@ -1,10 +1,13 @@ using System; +using System.Threading; using Avalonia.Controls; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; using Avalonia.Rendering; using MonoMac.AppKit; +using MonoMac.Foundation; +using MonoMac.ObjCRuntime; namespace Avalonia.MonoMac { @@ -16,9 +19,11 @@ namespace Avalonia.MonoMac internal static NSApplication App; private static bool s_monoMacInitialized; private static bool s_showInDock = true; + private static IRenderLoop s_renderLoop; void DoInitialize() { + InitializeMonoMac(); AvaloniaLocator.CurrentMutable .Bind().ToTransient() .Bind().ToSingleton() @@ -27,9 +32,8 @@ namespace Avalonia.MonoMac .Bind().ToConstant(this) .Bind().ToConstant(this) .Bind().ToSingleton() + .Bind().ToConstant(s_renderLoop) .Bind().ToConstant(PlatformThreadingInterface.Instance); - - InitializeMonoMac(); } public static void Initialize() @@ -39,13 +43,44 @@ namespace Avalonia.MonoMac } + + /// + /// See "Using POSIX Threads in a Cocoa Application" section here: + /// https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html#//apple_ref/doc/uid/20000738-125024 + /// + class ThreadHelper : NSObject + { + private readonly AutoResetEvent _event = new AutoResetEvent(false); + private const string InitThreadingName = "initThreading"; + [Export(InitThreadingName)] + public void DoNothing() + { + _event.Set(); + } + + public static void InitializeCocoaThreadingLocks() + { + var helper = new ThreadHelper(); + var thread = new NSThread(helper, Selector.FromHandle(Selector.GetHandle(InitThreadingName)), new NSObject()); + thread.Start(); + helper._event.WaitOne(); + helper._event.Dispose(); + if (!NSThread.IsMultiThreaded) + { + throw new Exception("Unable to initialize Cocoa threading"); + } + } + } + void InitializeMonoMac() { if(s_monoMacInitialized) return; NSApplication.Init(); + ThreadHelper.InitializeCocoaThreadingLocks(); App = NSApplication.SharedApplication; UpdateActivationPolicy(); + s_renderLoop = new RenderLoop(); //TODO: use CVDisplayLink s_monoMacInitialized = true; } @@ -64,6 +99,7 @@ namespace Avalonia.MonoMac } } + public static bool UseDeferredRendering { get; set; } = true; public Size DoubleClickSize => new Size(4, 4); public TimeSpan DoubleClickTime => TimeSpan.FromSeconds(NSEvent.DoubleClickInterval); @@ -87,9 +123,11 @@ namespace Avalonia { public static class MonoMacPlatformExtensions { - public static T UseMonoMac(this T builder) where T : AppBuilderBase, new() + public static T UseMonoMac(this T builder, bool? useDeferredRendering = null) where T : AppBuilderBase, new() { - return builder.UseWindowingSubsystem(MonoMac.MonoMacPlatform.Initialize); + if (useDeferredRendering.HasValue) + MonoMac.MonoMacPlatform.UseDeferredRendering = useDeferredRendering.Value; + return builder.UseWindowingSubsystem(MonoMac.MonoMacPlatform.Initialize, "MonoMac"); } } } \ No newline at end of file diff --git a/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs b/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs index 184416e77a..88189ba12c 100644 --- a/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs +++ b/src/OSX/Avalonia.MonoMac/PlatformThreadingInterface.cs @@ -3,14 +3,18 @@ using System.Threading; using Avalonia.Platform; using Avalonia.Threading; using MonoMac.AppKit; +using MonoMac.CoreFoundation; using MonoMac.CoreGraphics; using MonoMac.Foundation; +using MonoMac.ObjCRuntime; namespace Avalonia.MonoMac { - class PlatformThreadingInterface : IPlatformThreadingInterface + class PlatformThreadingInterface : NSObject, IPlatformThreadingInterface { private bool _signaled; + private const string SignaledSelectorName = "avaloniauiSignaled"; + private readonly Selector _signaledSelector = new Selector(SignaledSelectorName); public static PlatformThreadingInterface Instance { get; } = new PlatformThreadingInterface(); public bool CurrentThreadIsLoopThread => NSThread.Current.IsMainThread; @@ -27,18 +31,25 @@ namespace Avalonia.MonoMac return; _signaled = true; } - NSApplication.SharedApplication.BeginInvokeOnMainThread(() => - { - lock (this) + PerformSelector(_signaledSelector, NSThread.MainThread, this, false, + new[] { - if (!_signaled) - return; - _signaled = false; - } - Signaled?.Invoke(null); - }); + NSRunLoop.NSDefaultRunLoopMode, NSRunLoop.NSRunLoopEventTracking, NSRunLoop.NSRunLoopModalPanelMode, + NSRunLoop.NSRunLoopCommonModes, NSRunLoop.NSRunLoopConnectionReplyMode + }); } + [Export(SignaledSelectorName)] + public void CallSignaled() + { + lock (this) + { + if (!_signaled) + return; + _signaled = false; + } + Signaled?.Invoke(null); + } public void RunLoop(CancellationToken cancellationToken) @@ -55,6 +66,7 @@ namespace Avalonia.MonoMac var ev = app.NextEvent(NSEventMask.AnyEvent, NSDate.DistantFuture, NSRunLoop.NSDefaultRunLoopMode, true); if (ev != null) { + Console.WriteLine("NSEVENT"); app.SendEvent(ev); ev.Dispose(); } diff --git a/src/OSX/Avalonia.MonoMac/RenderLoop.cs b/src/OSX/Avalonia.MonoMac/RenderLoop.cs new file mode 100644 index 0000000000..a6f3e9f493 --- /dev/null +++ b/src/OSX/Avalonia.MonoMac/RenderLoop.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Platform; +using Avalonia.Rendering; +using MonoMac.Foundation; + +namespace Avalonia.MonoMac +{ + //TODO: Switch to using CVDisplayLink + public class RenderLoop : IRenderLoop + { + private readonly object _lock = new object(); + private readonly IDisposable _timer; + + public RenderLoop() + { + _timer = AvaloniaLocator.Current.GetService().StartSystemTimer(new TimeSpan(0, 0, 0, 0, 1000 / 60), + () => + { + lock (_lock) + { + using (new NSAutoreleasePool()) + { + Tick?.Invoke(this, EventArgs.Empty); + } + } + }); + } + + public event EventHandler Tick; + } +} diff --git a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs index 2744474214..5ea7972871 100644 --- a/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs +++ b/src/OSX/Avalonia.MonoMac/TopLevelImpl.cs @@ -5,8 +5,9 @@ using Avalonia.Input.Raw; using Avalonia.Platform; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Rendering; +using Avalonia.Threading; using MonoMac.AppKit; - +using MonoMac.CoreFoundation; using MonoMac.CoreGraphics; using MonoMac.Foundation; using MonoMac.ObjCRuntime; @@ -24,6 +25,7 @@ namespace Avalonia.MonoMac protected virtual void OnInput(RawInputEventArgs args) { + Dispatcher.UIThread.RunJobs(DispatcherPriority.Input + 1); Input?.Invoke(args); } @@ -36,6 +38,14 @@ namespace Avalonia.MonoMac private readonly IKeyboardDevice _keyboard; private NSTrackingArea _area; private NSCursor _cursor; + private bool _nonUiRedrawQueued; + + public CGSize PixelSize { get; set; } + + public CGSize LogicalSize { get; set; } + + private SavedImage _backBuffer; + public object SyncRoot { get; } = new object(); public TopLevelView(TopLevelImpl tl) { @@ -44,17 +54,75 @@ namespace Avalonia.MonoMac _keyboard = AvaloniaLocator.Current.GetService(); } + protected override void Dispose(bool disposing) + { + if (disposing) + { + _backBuffer?.Dispose(); + _backBuffer = null; + } + base.Dispose(disposing); + } + public override bool ConformsToProtocol(IntPtr protocol) { var rv = base.ConformsToProtocol(protocol); return rv; } + public override bool IsOpaque => false; + public override void DrawRect(CGRect dirtyRect) { + lock (SyncRoot) + _nonUiRedrawQueued = false; + Dispatcher.UIThread.RunJobs(DispatcherPriority.Render); + lock (SyncRoot) + { + if (_backBuffer != null) + { + using (var context = NSGraphicsContext.CurrentContext.GraphicsPort) + { + context.SetFillColor(255, 255, 255, 255); + context.FillRect(new CGRect(default(CGPoint), LogicalSize)); + context.TranslateCTM(0, LogicalSize.Height - _backBuffer.LogicalSize.Height); + context.DrawImage(new CGRect(default(CGPoint), _backBuffer.LogicalSize), _backBuffer.Image); + context.Flush(); + NSGraphicsContext.CurrentContext.FlushGraphics(); + } + } + } _tl.Paint?.Invoke(dirtyRect.ToAvaloniaRect()); } + public void SetBackBufferImage(SavedImage image) + { + lock (SyncRoot) + { + _backBuffer?.Dispose(); + _backBuffer = image; + if (image == null) + return; + + if (_nonUiRedrawQueued) + return; + _nonUiRedrawQueued = true; + Dispatcher.UIThread.InvokeAsync( + () => + { + lock (SyncRoot) + { + if (!_nonUiRedrawQueued) + return; + _nonUiRedrawQueued = false; + } + SetNeedsDisplayInRect(Frame); + Display(); + }, DispatcherPriority.Render); + + } + } + [Export("viewDidChangeBackingProperties:")] public void ViewDidChangeBackingProperties() { @@ -78,7 +146,12 @@ namespace Avalonia.MonoMac public override void SetFrameSize(CGSize newSize) { - base.SetFrameSize(newSize); + lock (SyncRoot) + { + base.SetFrameSize(newSize); + LogicalSize = Frame.Size; + PixelSize = ConvertSizeToBacking(LogicalSize); + } if (_area != null) { @@ -92,6 +165,7 @@ namespace Avalonia.MonoMac AddTrackingArea(_area); UpdateCursor(); _tl?.Resized?.Invoke(_tl.ClientSize); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); } InputModifiers GetModifiers(NSEventModifierMask mod) @@ -348,9 +422,16 @@ namespace Avalonia.MonoMac View.Dispose(); } - public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); + public IRenderer CreateRenderer(IRenderRoot root) => + MonoMacPlatform.UseDeferredRendering + ? new DeferredRenderer(root, AvaloniaLocator.Current.GetService()) + : (IRenderer) new ImmediateRenderer(root); - public void Invalidate(Rect rect) => View.SetNeedsDisplayInRect(View.Frame); + public void Invalidate(Rect rect) + { + if (!MonoMacPlatform.UseDeferredRendering) + View.SetNeedsDisplayInRect(View.Frame); + } public abstract Point PointToClient(Point point); diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 66b237de06..4d6559a078 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -428,7 +428,7 @@ namespace Avalonia.Base.UnitTests object anchor = null, bool enableDataValidation = false) { - return new InstancedBinding(_source, BindingMode.OneTime); + return InstancedBinding.OneTime(_source); } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index ccbf04533d..85a450e1dc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -249,6 +249,37 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() + { + using (CreateServices()) + { + TestControl child; + var popup = new Popup + { + Child = child = new TestControl(), + DataContext = "foo", + }; + + var beginCalled = false; + child.DataContextBeginUpdate += (s, e) => beginCalled = true; + + // Test for #1245. Here, the child's logical parent is the popup but it's not yet + // attached to a visual tree because the popup hasn't been opened. + Assert.Same(popup, ((ILogical)child).LogicalParent); + Assert.Same(popup, child.InheritanceParent); + Assert.Null(child.GetVisualRoot()); + + popup.Open(); + + // #1245 was caused by the fact that DataContextBeginUpdate was called on `target` + // when the PopupRoot was created, even though PopupRoot isn't the + // InheritanceParent of child. + Assert.False(beginCalled); + } + } + + private static IDisposable CreateServices() { var result = AvaloniaLocator.EnterScope(); @@ -304,5 +335,18 @@ namespace Avalonia.Controls.UnitTests.Primitives private class PopupContentControl : ContentControl { } + + private class TestControl : Decorator + { + public event EventHandler DataContextBeginUpdate; + + public new IAvaloniaObject InheritanceParent => base.InheritanceParent; + + protected override void OnDataContextBeginUpdate() + { + DataContextBeginUpdate?.Invoke(this, EventArgs.Empty); + base.OnDataContextBeginUpdate(); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 29eac3d8f5..17eccc96bc 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -515,7 +515,7 @@ namespace Avalonia.Controls.UnitTests public InstancedBinding ItemsSelector(object item) { var obs = new ExpressionObserver(item, nameof(Node.Children)); - return new InstancedBinding(obs); + return InstancedBinding.OneWay(obs); } public bool Match(object data) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs index 874dc18552..4450cc0c90 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/MultiBindingTests.cs @@ -34,8 +34,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data var target = new Mock().As(); target.Setup(x => x.GetValue(Control.DataContextProperty)).Returns(source); - var subject = binding.Initiate(target.Object, null).Subject; - var result = await subject.Take(1); + var observable = binding.Initiate(target.Object, null).Observable; + var result = await observable.Take(1); Assert.Equal("1,2,3", result); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index 8c7dad1bce..03dc5374e0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs @@ -411,8 +411,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml var xaml = @" - - + "; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 65fc9eaddd..b339972029 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -61,171 +61,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] - public void StyleResource_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 StyleResource_Can_Be_Assigned_To_Setter() - { - //skip default theme and styles, they are not needed - using (UnitTestApplication.Start(TestServices.StyledWindow - .With(theme: () => new Styles()))) - { - var xaml = @" - - - - - -