From 97db5ec6578a811837d7b28acbab66cdb7063d52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Oct 2018 16:34:50 +0200 Subject: [PATCH 01/46] Cache AvaloniaObject initialization notifications. When an `AvaloniaObject` is created, it notifies each of the `AvaloniaProperties` registered on it that they have been initialized on a new object. Instead of calling `GetDefaultValue` each time, cache the default values. --- src/Avalonia.Base/AvaloniaObject.cs | 46 +--------------- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 55 +++++++++++++++++++ 2 files changed, 57 insertions(+), 44 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7e8d733f1b..2a19f40ecb 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -22,27 +22,10 @@ namespace Avalonia /// public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { - /// - /// The parent object that inherited values are inherited from. - /// private IAvaloniaObject _inheritanceParent; - - /// - /// Maintains a list of direct property binding subscriptions so that the binding source - /// doesn't get collected. - /// private List _directBindings; - - /// - /// Event handler for implementation. - /// private PropertyChangedEventHandler _inpcChanged; - - /// - /// Event handler for implementation. - /// private EventHandler _propertyChanged; - private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); @@ -52,32 +35,7 @@ namespace Avalonia public AvaloniaObject() { VerifyAccess(); - - void Notify(AvaloniaProperty property) - { - object value = property.IsDirect ? - ((IDirectPropertyAccessor)property).GetValue(this) : - ((IStyledPropertyAccessor)property).GetDefaultValue(GetType()); - - var e = new AvaloniaPropertyChangedEventArgs( - this, - property, - AvaloniaProperty.UnsetValue, - value, - BindingPriority.Unset); - - property.NotifyInitialized(e); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegistered(this)) - { - Notify(property); - } - - foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())) - { - Notify(property); - } + AvaloniaPropertyRegistry.Instance.NotifyInitialized(this); } /// @@ -628,7 +586,7 @@ namespace Avalonia /// /// The property. /// The default value. - internal object GetDefaultValue(AvaloniaProperty property) + private object GetDefaultValue(AvaloniaProperty property) { if (property.Inherits && InheritanceParent is AvaloniaObject aobj) return aobj.GetValue(property); diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index e29e7339ae..af587ea1af 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using Avalonia.Data; namespace Avalonia { @@ -21,6 +22,8 @@ namespace Avalonia new Dictionary>(); private readonly Dictionary> _attachedCache = new Dictionary>(); + private readonly Dictionary>> _initializedCache = + new Dictionary>>(); /// /// Gets the instance @@ -204,6 +207,7 @@ namespace Avalonia } _registeredCache.Clear(); + _initializedCache.Clear(); } /// @@ -239,6 +243,57 @@ namespace Avalonia } _attachedCache.Clear(); + _initializedCache.Clear(); + } + + internal void NotifyInitialized(AvaloniaObject o) + { + Contract.Requires(o != null); + + var type = o.GetType(); + + void Notify(AvaloniaProperty property, object value) + { + var e = new AvaloniaPropertyChangedEventArgs( + o, + property, + AvaloniaProperty.UnsetValue, + value, + BindingPriority.Unset); + + property.NotifyInitialized(e); + } + + if (!_initializedCache.TryGetValue(type, out var items)) + { + var build = new Dictionary(); + + foreach (var property in GetRegistered(type)) + { + var value = !property.IsDirect ? + ((IStyledPropertyAccessor)property).GetDefaultValue(type) : + null; + build.Add(property, value); + } + + foreach (var property in GetRegisteredAttached(type)) + { + if (!build.ContainsKey(property)) + { + var value = ((IStyledPropertyAccessor)property).GetDefaultValue(type); + build.Add(property, value); + } + } + + items = build.ToList(); + _initializedCache.Add(type, items); + } + + foreach (var i in items) + { + var value = i.Key.IsDirect ? o.GetValue(i.Key) : i.Value; + Notify(i.Key, value); + } } } } From c5e4996da22376c9bc2c16c683b6f9ca60de48f8 Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 2 Jan 2019 21:16:08 +0100 Subject: [PATCH 02/46] Improved performance of value lookup in AvaloniaObject's ValueStore --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 16 +- src/Avalonia.Base/ValueStore.cs | 156 ++++++++++++++++-- 2 files changed, 156 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index e29e7339ae..7beab5f497 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -13,6 +13,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { + private readonly Dictionary _allProperties = + new Dictionary(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -148,6 +150,16 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } + /// + /// Finds a registered property by Id. + /// + /// The property Id. + /// The registered property or null if no matching property found. + public AvaloniaProperty FindRegistered(int id) + { + return _allProperties.TryGetValue(id, out var value) ? value : null; + } + /// /// Checks whether a is registered on a type. /// @@ -202,7 +214,8 @@ namespace Avalonia { inner.Add(property.Id, property); } - + + _allProperties[property.Id] = property; _registeredCache.Clear(); } @@ -238,6 +251,7 @@ namespace Avalonia inner.Add(property.Id, property); } + _allProperties[property.Id] = property; _attachedCache.Clear(); } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index adbe89aceb..f78aaab221 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,13 +7,21 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { + struct Entry + { + internal int PropertyId; + internal object Value; + } + private readonly AvaloniaObject _owner; - private readonly Dictionary _values = - new Dictionary(); + private Entry[] _entries; public ValueStore(AvaloniaObject owner) { _owner = owner; + + // The last item in the list is always int.MaxValue + _entries = new[] { new Entry { PropertyId = int.MaxValue, Value = null } }; } public IDisposable AddBinding( @@ -23,7 +31,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (_values.TryGetValue(property, out var v)) + if (TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -31,13 +39,13 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _values[property] = priorityValue; + SetValueInternal(property, priorityValue); } } else { priorityValue = CreatePriorityValue(property); - _values.Add(property, priorityValue); + AddValueInternal(property, priorityValue); } return priorityValue.Add(source, (int)priority); @@ -47,7 +55,7 @@ namespace Avalonia { PriorityValue priorityValue; - if (_values.TryGetValue(property, out var v)) + if (TryGetValue(property, out var v)) { priorityValue = v as PriorityValue; @@ -55,7 +63,7 @@ namespace Avalonia { if (priority == (int)BindingPriority.LocalValue) { - _values[property] = Validate(property, value); + SetValueInternal(property, Validate(property, value)); Changed(property, priority, v, value); return; } @@ -63,7 +71,7 @@ namespace Avalonia { priorityValue = CreatePriorityValue(property); priorityValue.SetValue(v, (int)BindingPriority.LocalValue); - _values[property] = priorityValue; + SetValueInternal(property, priorityValue); } } } @@ -76,14 +84,14 @@ namespace Avalonia if (priority == (int)BindingPriority.LocalValue) { - _values.Add(property, Validate(property, value)); + AddValueInternal(property, Validate(property, value)); Changed(property, priority, AvaloniaProperty.UnsetValue, value); return; } else { priorityValue = CreatePriorityValue(property); - _values.Add(property, priorityValue); + AddValueInternal(property, priorityValue); } } @@ -100,13 +108,22 @@ namespace Avalonia _owner.PriorityValueChanged(property, priority, oldValue, newValue); } - public IDictionary GetSetValues() => _values; + public IDictionary GetSetValues() + { + var dict = new Dictionary(_entries.Length - 1); + for (int i = 0; i < _entries.Length - 1; ++i) + { + dict.Add(AvaloniaPropertyRegistry.Instance.FindRegistered(_entries[i].PropertyId), _entries[i].Value); + } + + return dict; + } public object GetValue(AvaloniaProperty property) { var result = AvaloniaProperty.UnsetValue; - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { result = (value is PriorityValue priorityValue) ? priorityValue.Value : value; } @@ -116,12 +133,12 @@ namespace Avalonia public bool IsAnimating(AvaloniaProperty property) { - return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; + return TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating; } public bool IsSet(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { return ((value as PriorityValue)?.Value ?? value) != AvaloniaProperty.UnsetValue; } @@ -131,7 +148,7 @@ namespace Avalonia public void Revalidate(AvaloniaProperty property) { - if (_values.TryGetValue(property, out var value)) + if (TryGetValue(property, out var value)) { (value as PriorityValue)?.Revalidate(); } @@ -178,5 +195,114 @@ namespace Avalonia (_deferredSetter = new DeferredSetter()); } } + + private bool TryGetValue(AvaloniaProperty property, out object value) + { + (int index, bool found) = TryFindEntry(property.Id); + if (!found) + { + value = null; + return false; + } + + value = _entries[index].Value; + return true; + } + + private void AddValueInternal(AvaloniaProperty property, object value) + { + Entry[] entries = new Entry[_entries.Length + 1]; + + for (int i = 0; i < _entries.Length; ++i) + { + if (_entries[i].PropertyId > property.Id) + { + if (i > 0) + { + Array.Copy(_entries, 0, entries, 0, i); + } + + entries[i] = new Entry { PropertyId = property.Id, Value = value }; + Array.Copy(_entries, i, entries, i + 1, _entries.Length - i); + break; + } + } + + _entries = entries; + } + + private void SetValueInternal(AvaloniaProperty property, object value) + { + _entries[TryFindEntry(property.Id).Item1].Value = value; + } + + private (int, bool) TryFindEntry(int propertyId) + { + if (_entries.Length <= 20) + { + // For small lists, we use an optimized linear search. Since the last item in the list + // is always int.MaxValue, we can skip a conditional branch in each iteration. + // By unrolling the loop, we can skip another unconditional branch in each iteration. + + if (_entries[0].PropertyId >= propertyId) return (0, _entries[0].PropertyId == propertyId); + if (_entries[1].PropertyId >= propertyId) return (1, _entries[1].PropertyId == propertyId); + if (_entries[2].PropertyId >= propertyId) return (2, _entries[2].PropertyId == propertyId); + if (_entries[3].PropertyId >= propertyId) return (3, _entries[3].PropertyId == propertyId); + if (_entries[4].PropertyId >= propertyId) return (4, _entries[4].PropertyId == propertyId); + if (_entries[5].PropertyId >= propertyId) return (5, _entries[5].PropertyId == propertyId); + if (_entries[6].PropertyId >= propertyId) return (6, _entries[6].PropertyId == propertyId); + if (_entries[7].PropertyId >= propertyId) return (7, _entries[7].PropertyId == propertyId); + if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); + if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); + if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); + if (_entries[11].PropertyId >= propertyId) return (11, _entries[11].PropertyId == propertyId); + if (_entries[12].PropertyId >= propertyId) return (12, _entries[12].PropertyId == propertyId); + if (_entries[13].PropertyId >= propertyId) return (13, _entries[13].PropertyId == propertyId); + if (_entries[14].PropertyId >= propertyId) return (14, _entries[14].PropertyId == propertyId); + if (_entries[15].PropertyId >= propertyId) return (15, _entries[15].PropertyId == propertyId); + if (_entries[16].PropertyId >= propertyId) return (16, _entries[16].PropertyId == propertyId); + if (_entries[17].PropertyId >= propertyId) return (17, _entries[17].PropertyId == propertyId); + if (_entries[18].PropertyId >= propertyId) return (18, _entries[18].PropertyId == propertyId); + } + else + { + int low = 0; + int high = _entries.Length; + int id; + + if (high > 0) + { + while (high - low > 3) + { + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; + + if (propertyId == id) + return (pivot, true); + + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } + + do + { + id = _entries[low].PropertyId; + + if (id == propertyId) + return (low, true); + + if (id > propertyId) + break; + + ++low; + } + while (low < high); + } + } + + return (0, false); + } } } From 814222a15dadda3442b8c970da70e401ad313d87 Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 9 Jan 2019 22:21:40 +0100 Subject: [PATCH 03/46] Reduced linear search, made FindRegistered method internal --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 2 +- src/Avalonia.Base/ValueStore.cs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 7beab5f497..dc75727941 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -155,7 +155,7 @@ namespace Avalonia /// /// The property Id. /// The registered property or null if no matching property found. - public AvaloniaProperty FindRegistered(int id) + internal AvaloniaProperty FindRegistered(int id) { return _allProperties.TryGetValue(id, out var value) ? value : null; } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index f78aaab221..7dad72b551 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -7,7 +7,7 @@ namespace Avalonia { internal class ValueStore : IPriorityValueOwner { - struct Entry + private struct Entry { internal int PropertyId; internal object Value; @@ -238,7 +238,7 @@ namespace Avalonia private (int, bool) TryFindEntry(int propertyId) { - if (_entries.Length <= 20) + if (_entries.Length <= 16) { // For small lists, we use an optimized linear search. Since the last item in the list // is always int.MaxValue, we can skip a conditional branch in each iteration. @@ -259,10 +259,6 @@ namespace Avalonia if (_entries[12].PropertyId >= propertyId) return (12, _entries[12].PropertyId == propertyId); if (_entries[13].PropertyId >= propertyId) return (13, _entries[13].PropertyId == propertyId); if (_entries[14].PropertyId >= propertyId) return (14, _entries[14].PropertyId == propertyId); - if (_entries[15].PropertyId >= propertyId) return (15, _entries[15].PropertyId == propertyId); - if (_entries[16].PropertyId >= propertyId) return (16, _entries[16].PropertyId == propertyId); - if (_entries[17].PropertyId >= propertyId) return (17, _entries[17].PropertyId == propertyId); - if (_entries[18].PropertyId >= propertyId) return (18, _entries[18].PropertyId == propertyId); } else { From 5cbe89e9d6d6ba51acfb4aa7b3881f2f8158119b Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 16 Jan 2019 20:01:38 +0100 Subject: [PATCH 04/46] Switched AvaloniaPropertyRegistry._properties from Dictionary to List --- src/Avalonia.Base/AvaloniaProperty.cs | 2 +- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 10 ++-- src/Avalonia.Base/ValueStore.cs | 47 ++++++++----------- 3 files changed, 26 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 4b0116a536..953132116c 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -21,7 +21,7 @@ namespace Avalonia /// public static readonly object UnsetValue = new Unset(); - private static int s_nextId = 1; + private static int s_nextId; private readonly Subject _initialized; private readonly Subject _changed; private readonly PropertyMetadata _defaultMetadata; diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index dc75727941..11b1096052 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -13,8 +13,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { - private readonly Dictionary _allProperties = - new Dictionary(); + private readonly IList _properties = + new List(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -157,7 +157,7 @@ namespace Avalonia /// The registered property or null if no matching property found. internal AvaloniaProperty FindRegistered(int id) { - return _allProperties.TryGetValue(id, out var value) ? value : null; + return id < _properties.Count ? _properties[id] : null; } /// @@ -215,7 +215,7 @@ namespace Avalonia inner.Add(property.Id, property); } - _allProperties[property.Id] = property; + _properties.Add(property); _registeredCache.Clear(); } @@ -251,7 +251,7 @@ namespace Avalonia inner.Add(property.Id, property); } - _allProperties[property.Id] = property; + _properties.Add(property); _attachedCache.Clear(); } } diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 7dad72b551..d520e2b80a 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/src/Avalonia.Base/ValueStore.cs @@ -238,7 +238,7 @@ namespace Avalonia private (int, bool) TryFindEntry(int propertyId) { - if (_entries.Length <= 16) + if (_entries.Length <= 12) { // For small lists, we use an optimized linear search. Since the last item in the list // is always int.MaxValue, we can skip a conditional branch in each iteration. @@ -255,10 +255,6 @@ namespace Avalonia if (_entries[8].PropertyId >= propertyId) return (8, _entries[8].PropertyId == propertyId); if (_entries[9].PropertyId >= propertyId) return (9, _entries[9].PropertyId == propertyId); if (_entries[10].PropertyId >= propertyId) return (10, _entries[10].PropertyId == propertyId); - if (_entries[11].PropertyId >= propertyId) return (11, _entries[11].PropertyId == propertyId); - if (_entries[12].PropertyId >= propertyId) return (12, _entries[12].PropertyId == propertyId); - if (_entries[13].PropertyId >= propertyId) return (13, _entries[13].PropertyId == propertyId); - if (_entries[14].PropertyId >= propertyId) return (14, _entries[14].PropertyId == propertyId); } else { @@ -266,36 +262,33 @@ namespace Avalonia int high = _entries.Length; int id; - if (high > 0) + while (high - low > 3) { - while (high - low > 3) - { - int pivot = (high + low) / 2; - id = _entries[pivot].PropertyId; + int pivot = (high + low) / 2; + id = _entries[pivot].PropertyId; - if (propertyId == id) - return (pivot, true); + if (propertyId == id) + return (pivot, true); - if (propertyId <= id) - high = pivot; - else - low = pivot + 1; - } + if (propertyId <= id) + high = pivot; + else + low = pivot + 1; + } - do - { - id = _entries[low].PropertyId; + do + { + id = _entries[low].PropertyId; - if (id == propertyId) - return (low, true); + if (id == propertyId) + return (low, true); - if (id > propertyId) - break; + if (id > propertyId) + break; - ++low; - } - while (low < high); + ++low; } + while (low < high); } return (0, false); From f3029d33463494ed60c8d27aa4ebb1cb918534f2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 19 Jan 2019 18:04:30 +0100 Subject: [PATCH 05/46] Added failing test for #2203. --- .../ExpressionObserverTests_Observable.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs index 701fdbce9c..4585181ab7 100644 --- a/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs +++ b/tests/Avalonia.Base.UnitTests/Data/Core/ExpressionObserverTests_Observable.cs @@ -150,6 +150,26 @@ namespace Avalonia.Base.UnitTests.Data.Core } } + [Fact] + public void Should_Work_With_Value_Type() + { + using (var sync = UnitTestSynchronizationContext.Begin()) + { + var source = new BehaviorSubject(1); + var data = new { Foo = source }; + var target = ExpressionObserver.Create(data, o => o.Foo.StreamBinding()); + var result = new List(); + + var sub = target.Subscribe(x => result.Add((int)x)); + source.OnNext(42); + sync.ExecutePostedCallbacks(); + + Assert.Equal(new[] { 1, 42 }, result); + + GC.KeepAlive(data); + } + } + private class Class1 : NotifyingBase { public Subject Next { get; } = new Subject(); From d3e8752f15eb90db33707aa592e681ba37fda1a7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 19 Jan 2019 18:05:13 +0100 Subject: [PATCH 06/46] Make ObservableStreamPlugin work with value types. Use reflection to call `Observable.Select` on the source observable to box the value. --- .../Core/Plugins/ObservableStreamPlugin.cs | 76 ++++++++++++++++++- 1 file changed, 74 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs index 14ca8ee79e..c41097c274 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/ObservableStreamPlugin.cs @@ -2,6 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; +using System.Reactive.Linq; +using System.Reflection; namespace Avalonia.Data.Core.Plugins { @@ -10,12 +13,19 @@ namespace Avalonia.Data.Core.Plugins /// public class ObservableStreamPlugin : IStreamPlugin { + static MethodInfo observableSelect; + /// /// Checks whether this plugin handles the specified value. /// /// A weak reference to the value. /// True if the plugin can handle the value; otherwise false. - public virtual bool Match(WeakReference reference) => reference.Target is IObservable; + public virtual bool Match(WeakReference reference) + { + return reference.Target.GetType().GetInterfaces().Any(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IObservable<>)); + } /// /// Starts producing output based on the specified value. @@ -26,7 +36,69 @@ namespace Avalonia.Data.Core.Plugins /// public virtual IObservable Start(WeakReference reference) { - return reference.Target as IObservable; + var target = reference.Target; + + // If the observable returns a reference type then we can cast it. + if (target is IObservable result) + { + return result; + }; + + // If the observable returns a value type then we need to call Observable.Select on it. + // First get the type of T in `IObservable`. + var sourceType = reference.Target.GetType().GetInterfaces().First(x => + x.IsGenericType && + x.GetGenericTypeDefinition() == typeof(IObservable<>)).GetGenericArguments()[0]; + + // Get the Observable.Select method. + var select = GetObservableSelect(sourceType); + + // Make a Box<> delegate of the correct type. + var funcType = typeof(Func<,>).MakeGenericType(sourceType, typeof(object)); + var box = GetType().GetMethod(nameof(Box), BindingFlags.Static | BindingFlags.NonPublic) + .MakeGenericMethod(sourceType) + .CreateDelegate(funcType); + + // Call Observable.Select(target, box); + return (IObservable)select.Invoke( + null, + new object[] { target, box }); + } + + private static MethodInfo GetObservableSelect(Type source) + { + return GetObservableSelect().MakeGenericMethod(source, typeof(object)); } + + private static MethodInfo GetObservableSelect() + { + if (observableSelect == null) + { + observableSelect = typeof(Observable).GetRuntimeMethods().First(x => + { + if (x.Name == nameof(Observable.Select) && + x.ContainsGenericParameters && + x.GetGenericArguments().Length == 2) + { + var parameters = x.GetParameters(); + + if (parameters.Length == 2 && + parameters[0].ParameterType.IsConstructedGenericType && + parameters[0].ParameterType.GetGenericTypeDefinition() == typeof(IObservable<>) && + parameters[1].ParameterType.IsConstructedGenericType && + parameters[1].ParameterType.GetGenericTypeDefinition() == typeof(Func<,>)) + { + return true; + } + } + + return false; + }); + } + + return observableSelect; + } + + private static object Box(T value) => (object)value; } } From 9501da85fa4da89dc0d7e11eaff9316d42da523c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Jan 2019 09:19:27 +0100 Subject: [PATCH 07/46] Added failing tests for #2260. --- .../Avalonia.Controls.UnitTests/ImageTests.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ImageTests.cs b/tests/Avalonia.Controls.UnitTests/ImageTests.cs index e92fc572b4..71d0d1e328 100644 --- a/tests/Avalonia.Controls.UnitTests/ImageTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ImageTests.cs @@ -61,5 +61,61 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(50, 50), target.DesiredSize); } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_No_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.None; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 100, 400)); + + Assert.Equal(new Size(50, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Fill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Fill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_Uniform_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.Uniform; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 50), target.Bounds.Size); + } + + [Fact] + public void Arrange_Should_Return_Correct_Size_For_UniformToFill_Stretch() + { + var bitmap = Mock.Of(x => x.PixelSize == new PixelSize(50, 100)); + var target = new Image(); + target.Stretch = Stretch.UniformToFill; + target.Source = bitmap; + + target.Measure(new Size(50, 50)); + target.Arrange(new Rect(0, 0, 25, 100)); + + Assert.Equal(new Size(25, 100), target.Bounds.Size); + } } } From 3ee48b25e4ecd967227694f96a85a89aa36b4217 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 23 Jan 2019 09:09:44 +0100 Subject: [PATCH 08/46] Fix image arrange. `Image` was calculating its desired size correctly according to the value of `Stretch` but then was not applying that calculation to the arrange pass. Fixes #2260 --- src/Avalonia.Controls/Image.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index 72379e7b53..c696fe7975 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -99,5 +99,22 @@ namespace Avalonia.Controls return new Size(); } } + + /// + protected override Size ArrangeOverride(Size finalSize) + { + var source = Source; + + if (source != null) + { + var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height); + var result = Stretch.CalculateSize(finalSize, sourceSize); + return result; + } + else + { + return new Size(); + } + } } } From 533284cd616a91c911951effc50f397f9fc0232d Mon Sep 17 00:00:00 2001 From: Artyom Date: Thu, 24 Jan 2019 15:30:05 +0300 Subject: [PATCH 09/46] Fix AvaloniaProperty registration type --- src/Avalonia.ReactiveUI/ReactiveUserControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs index 04c6f100a6..43e2ef93b6 100644 --- a/src/Avalonia.ReactiveUI/ReactiveUserControl.cs +++ b/src/Avalonia.ReactiveUI/ReactiveUserControl.cs @@ -16,7 +16,7 @@ namespace Avalonia public class ReactiveUserControl : UserControl, IViewFor where TViewModel : class { public static readonly AvaloniaProperty ViewModelProperty = AvaloniaProperty - .Register, TViewModel>(nameof(ViewModel)); + .Register, TViewModel>(nameof(ViewModel)); /// /// Initializes a new instance of the class. @@ -41,4 +41,4 @@ namespace Avalonia set => ViewModel = (TViewModel)value; } } -} \ No newline at end of file +} From 81846e87ece5f0a6c5e7a9146a46a941fdf79a29 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Jan 2019 19:11:39 +0100 Subject: [PATCH 10/46] Added TopLevel.Opened event. And raise the event when a window is opened. --- src/Avalonia.Controls/TopLevel.cs | 11 +++++++++++ src/Avalonia.Controls/Window.cs | 2 ++ src/Avalonia.Controls/WindowBase.cs | 3 ++- 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 5ca3647da7..32c40847c5 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -136,6 +136,11 @@ namespace Avalonia.Controls } } + /// + /// Fired when the window is opened. + /// + public event EventHandler Opened; + /// /// Fired when the window is closed. /// @@ -311,6 +316,12 @@ namespace Avalonia.Controls $"Control '{GetType().Name}' is a top level control and cannot be added as a child."); } + /// + /// Raises the event. + /// + /// The event args. + protected virtual void OnOpened(EventArgs e) => Opened?.Invoke(this, e); + /// /// Tries to get a service from an , logging a /// warning if not found. diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 53f727900b..f5af6774b5 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -389,6 +389,7 @@ namespace Avalonia.Controls Renderer?.Start(); } SetWindowStartupLocation(Owner?.PlatformImpl); + OnOpened(EventArgs.Empty); } /// @@ -458,6 +459,7 @@ namespace Avalonia.Controls owner.Activate(); result.SetResult((TResult)(_dialogResult ?? default(TResult))); }); + OnOpened(EventArgs.Empty); } SetWindowStartupLocation(owner); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 2fba8619c6..56ffd315f1 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -163,7 +163,7 @@ namespace Avalonia.Controls } /// - /// Shows the popup. + /// Shows the window. /// public virtual void Show() { @@ -181,6 +181,7 @@ namespace Avalonia.Controls } PlatformImpl?.Show(); Renderer?.Start(); + OnOpened(EventArgs.Empty); } finally { From 6a167a688271e37b0e3c5ce34655dc3699ccc8bc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Jan 2019 19:11:57 +0100 Subject: [PATCH 11/46] Use Window.Opened event for rxui activation. --- src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs index cf386a235e..e1db604e95 100644 --- a/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs +++ b/src/Avalonia.ReactiveUI/AvaloniaActivationForViewFetcher.cs @@ -29,8 +29,8 @@ namespace Avalonia { var windowLoaded = Observable .FromEventPattern( - x => window.Initialized += x, - x => window.Initialized -= x) + x => window.Opened += x, + x => window.Opened -= x) .Select(args => true); var windowUnloaded = Observable .FromEventPattern( @@ -59,4 +59,4 @@ namespace Avalonia .DistinctUntilChanged(); } } -} \ No newline at end of file +} From 068acb63769a2a8636715d7a2a4396b93d19db8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Jan 2019 19:18:12 +0100 Subject: [PATCH 12/46] Added TopLevel.Opened unit tests. --- .../WindowBaseTests.cs | 16 ++++++++++++++ .../WindowTests.cs | 21 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 51a4d21392..6d00409ae0 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -199,6 +199,22 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Showing_Should_Raise_Opened() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new TestWindowBase(); + var raised = false; + + target.Opened += (s, e) => raised = true; + + target.Show(); + + Assert.True(raised); + } + } + [Fact] public void Hiding_Should_Stop_Renderer() { diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index c0b5342934..8221dadc86 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -228,18 +228,35 @@ namespace Avalonia.Controls.UnitTests [Fact] public void ShowDialog_Should_Start_Renderer() { - using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var parent = Mock.Of(); var renderer = new Mock(); var target = new Window(CreateImpl(renderer)); - target.Show(); + target.ShowDialog(parent); renderer.Verify(x => x.Start(), Times.Once); } } + [Fact] + public void ShowDialog_Should_Raise_Opened() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parent = Mock.Of(); + var target = new Window(); + var raised = false; + + target.Opened += (s, e) => raised = true; + + target.ShowDialog(parent); + + Assert.True(raised); + } + } + [Fact] public void Hiding_Should_Stop_Renderer() { From 163abb8322baa55741a00a477422e0b21879208a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 25 Jan 2019 12:54:29 +0100 Subject: [PATCH 13/46] Added Avalonia.ReactiveUI.UnitTests to solution. --- Avalonia.sln | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Avalonia.sln b/Avalonia.sln index 2f7560049c..d6472503fe 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -196,9 +196,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "_build", "nukebuild\_build. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Animation.UnitTests", "tests\Avalonia.Animation.UnitTests\Avalonia.Animation.UnitTests.csproj", "{AF227847-E65C-4BE9-BCE9-B551357788E0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.X11", "src\Avalonia.X11\Avalonia.X11.csproj", "{41B02319-965D-4945-8005-C1A3D1224165}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PlatformSanityChecks", "samples\PlatformSanityChecks\PlatformSanityChecks.csproj", "{D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.UnitTests", "tests\Avalonia.ReactiveUI.UnitTests\Avalonia.ReactiveUI.UnitTests.csproj", "{AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -1819,6 +1821,30 @@ Global {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhone.Build.0 = Release|Any CPU {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhone.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|Any CPU.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhone.Build.0 = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1875,6 +1901,7 @@ Global {AF227847-E65C-4BE9-BCE9-B551357788E0} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {41B02319-965D-4945-8005-C1A3D1224165} = {86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B} {D775DECB-4E00-4ED5-A75A-5FCE58ADFF0B} = {9B9E3891-2366-4253-A952-D08BCEB71098} + {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} From 653fa458c16d8734b6d918c14a9e810bad76e026 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 25 Jan 2019 12:55:16 +0100 Subject: [PATCH 14/46] Call InitializeComponent in RxUI activation tests. To make sure activation works after loading XAML. --- .../AvaloniaActivationForViewFetcherTest.cs | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs index b782311729..70a5504a7d 100644 --- a/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs +++ b/tests/Avalonia.ReactiveUI.UnitTests/AvaloniaActivationForViewFetcherTest.cs @@ -10,6 +10,7 @@ using ReactiveUI; using DynamicData; using Xunit; using Splat; +using Avalonia.Markup.Xaml; namespace Avalonia { @@ -70,12 +71,40 @@ namespace Avalonia public class ActivatableWindow : ReactiveWindow { - public ActivatableWindow() => this.WhenActivated(disposables => { }); + public ActivatableWindow() + { + InitializeComponent(); + Assert.IsType(Content); + this.WhenActivated(disposables => { }); + } + + private void InitializeComponent() + { + var loader = new AvaloniaXamlLoader(); + loader.Load(@" + + +", null, this); + } } public class ActivatableUserControl : ReactiveUserControl { - public ActivatableUserControl() => this.WhenActivated(disposables => { }); + public ActivatableUserControl() + { + InitializeComponent(); + Assert.IsType(Content); + this.WhenActivated(disposables => { }); + } + + private void InitializeComponent() + { + var loader = new AvaloniaXamlLoader(); + loader.Load(@" + + +", null, this); + } } public AvaloniaActivationForViewFetcherTest() @@ -183,4 +212,4 @@ namespace Avalonia Assert.False(viewModel.IsActivated); } } -} \ No newline at end of file +} From 233adc9ca5253e16ea5bc76f8984d922e5770e09 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 25 Jan 2019 17:51:48 +0100 Subject: [PATCH 15/46] Added `:not()` style selector. --- src/Avalonia.Styling/Styling/NotSelector.cs | 72 +++++++++++ src/Avalonia.Styling/Styling/Selectors.cs | 11 ++ .../Markup/Parsers/SelectorGrammar.cs | 51 +++++++- .../Markup/Parsers/SelectorParser.cs | 13 +- .../Parsers/SelectorGrammarTests.cs | 73 +++++++++++ .../Xaml/StyleTests.cs | 28 +++++ .../SelectorTests_Not.cs | 114 ++++++++++++++++++ 7 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 src/Avalonia.Styling/Styling/NotSelector.cs create mode 100644 tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs diff --git a/src/Avalonia.Styling/Styling/NotSelector.cs b/src/Avalonia.Styling/Styling/NotSelector.cs new file mode 100644 index 0000000000..bcf76620be --- /dev/null +++ b/src/Avalonia.Styling/Styling/NotSelector.cs @@ -0,0 +1,72 @@ +// 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.Linq; + +namespace Avalonia.Styling +{ + /// + /// The `:not()` style selector. + /// + internal class NotSelector : Selector + { + private readonly Selector _previous; + private readonly Selector _argument; + private string _selectorString; + + /// + /// Initializes a new instance of the class. + /// + /// The previous selector. + /// The selector to be not-ed. + public NotSelector(Selector previous, Selector argument) + { + _previous = previous; + _argument = argument ?? throw new InvalidOperationException("Not selector must have a selector argument."); + } + + /// + public override bool InTemplate => _argument.InTemplate; + + /// + public override bool IsCombinator => false; + + /// + public override Type TargetType => _previous?.TargetType; + + /// + public override string ToString() + { + if (_selectorString == null) + { + _selectorString = ":not(" + _argument.ToString() + ")"; + } + + return _selectorString; + } + + protected override SelectorMatch Evaluate(IStyleable control, bool subscribe) + { + var innerResult = _argument.Match(control, subscribe); + + switch (innerResult.Result) + { + case SelectorMatchResult.AlwaysThisInstance: + return SelectorMatch.NeverThisInstance; + case SelectorMatchResult.AlwaysThisType: + return SelectorMatch.NeverThisType; + case SelectorMatchResult.NeverThisInstance: + return SelectorMatch.AlwaysThisInstance; + case SelectorMatchResult.NeverThisType: + return SelectorMatch.AlwaysThisType; + case SelectorMatchResult.Sometimes: + return new SelectorMatch(innerResult.Activator.Select(x => !x)); + default: + throw new InvalidOperationException("Invalid SelectorMatchResult."); + } + } + + protected override Selector MovePrevious() => _previous; + } +} diff --git a/src/Avalonia.Styling/Styling/Selectors.cs b/src/Avalonia.Styling/Styling/Selectors.cs index c91cc7af04..4284c7e798 100644 --- a/src/Avalonia.Styling/Styling/Selectors.cs +++ b/src/Avalonia.Styling/Styling/Selectors.cs @@ -94,6 +94,17 @@ namespace Avalonia.Styling } } + /// + /// Returns a selector which inverts the results of selector argument. + /// + /// The previous selector. + /// The selector to be not-ed. + /// The selector. + public static Selector Not(this Selector previous, Func argument) + { + return new NotSelector(previous, argument(null)); + } + /// /// Returns a selector which matches a type. /// diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs index f66d3e51fc..55c3aab81f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -32,6 +33,11 @@ namespace Avalonia.Markup.Parsers public static IEnumerable Parse(string s) { var r = new CharacterReader(s.AsSpan()); + return Parse(ref r, null); + } + + private static IEnumerable Parse(ref CharacterReader r, char? end) + { var state = State.Start; var selector = new List(); while (!r.End && state != State.End) @@ -43,7 +49,7 @@ namespace Avalonia.Markup.Parsers state = ParseStart(ref r); break; case State.Middle: - state = ParseMiddle(ref r); + state = ParseMiddle(ref r, end); break; case State.CanHaveType: state = ParseCanHaveType(ref r); @@ -107,7 +113,7 @@ namespace Avalonia.Markup.Parsers return State.TypeName; } - private static State ParseMiddle(ref CharacterReader r) + private static State ParseMiddle(ref CharacterReader r, char? end) { if (r.TakeIf(':')) { @@ -129,6 +135,10 @@ namespace Avalonia.Markup.Parsers { return State.Name; } + else if (end.HasValue && !r.End && r.Peek == end.Value) + { + return State.End; + } return State.TypeName; } @@ -151,16 +161,23 @@ namespace Avalonia.Markup.Parsers } const string IsKeyword = "is"; + const string NotKeyword = "not"; + if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('(')) { var syntax = ParseType(ref r, new IsSyntax()); - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, $"Expected ')', got {r.Peek}"); - } + Expect(ref r, ')'); return (State.CanHaveType, syntax); } + if (identifier.SequenceEqual(NotKeyword.AsSpan()) && r.TakeIf('(')) + { + var argument = Parse(ref r, ')'); + Expect(ref r, ')'); + + var syntax = new NotSyntax { Argument = argument }; + return (State.Middle, syntax); + } else { return ( @@ -282,6 +299,18 @@ namespace Avalonia.Markup.Parsers return syntax; } + private static void Expect(ref CharacterReader r, char c) + { + if (r.End) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got end of selector."); + } + else if (!r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, $"Expected '{c}', got '{r.Peek}'."); + } + } + public interface ISyntax { } @@ -376,5 +405,15 @@ namespace Avalonia.Markup.Parsers return obj is TemplateSyntax; } } + + public class NotSyntax : ISyntax + { + public IEnumerable Argument { get; set; } + + public override bool Equals(object obj) + { + return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument); + } + } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs index bf5b396bec..8d1216e1dc 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs @@ -2,6 +2,7 @@ // 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.Globalization; using Avalonia.Styling; using Avalonia.Utilities; @@ -25,7 +26,7 @@ namespace Avalonia.Markup.Parsers /// public SelectorParser(Func typeResolver) { - this._typeResolver = typeResolver; + _typeResolver = typeResolver; } /// @@ -36,6 +37,11 @@ namespace Avalonia.Markup.Parsers public Selector Parse(string s) { var syntax = SelectorGrammar.Parse(s); + return Create(syntax); + } + + private Selector Create(IEnumerable syntax) + { var result = default(Selector); foreach (var i in syntax) @@ -97,6 +103,11 @@ namespace Avalonia.Markup.Parsers case SelectorGrammar.TemplateSyntax template: result = result.Template(); break; + case SelectorGrammar.NotSyntax not: + result = result.Not(x => Create(not.Argument)); + break; + default: + throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'."); } } diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs index 88fe5a2a12..e3ce4b0968 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs @@ -200,6 +200,67 @@ namespace Avalonia.Markup.UnitTests.Parsers result); } + [Fact] + public void Not_OfType() + { + var result = SelectorGrammar.Parse(":not(Button)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + }, + } + }, + result); + } + + [Fact] + public void OfType_Not_Class() + { + var result = SelectorGrammar.Parse("Button:not(.foo)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + } + }, + result); + } + + [Fact] + public void Is_Descendent_Not_OfType_Class() + { + var result = SelectorGrammar.Parse(":is(Control) :not(Button.foo)"); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.IsSyntax { TypeName = "Control" }, + new SelectorGrammar.DescendantSyntax { }, + new SelectorGrammar.NotSyntax + { + Argument = new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + } + }, + result); + } + [Fact] public void Namespace_Alone_Fails() { @@ -223,5 +284,17 @@ namespace Avalonia.Markup.UnitTests.Parsers { Assert.Throws(() => SelectorGrammar.Parse(".%foo")); } + + [Fact] + public void Not_Without_Argument_Fails() + { + Assert.Throws(() => SelectorGrammar.Parse(":not()")); + } + + [Fact] + public void Not_Without_Closing_Parenthesis_Fails() + { + Assert.Throws(() => SelectorGrammar.Parse(":not(Button")); + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index beaf7477d0..a84ce74a88 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -198,5 +198,33 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml ex.InnerException.Message); } } + + [Fact] + public void Style_Can_Use_Not_Selector() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var foo = window.FindControl("foo"); + var notFoo = window.FindControl("notFoo"); + + Assert.Null(foo.Background); + Assert.Equal(Colors.Red, ((ISolidColorBrush)notFoo.Background).Color); + } + } } } diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs new file mode 100644 index 0000000000..2f3e2b8f34 --- /dev/null +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Not.cs @@ -0,0 +1,114 @@ +// 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.Reactive.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Styling.UnitTests +{ + public class SelectorTests_Not + { + [Fact] + public void Not_Selector_Should_Have_Correct_String_Representation() + { + var target = default(Selector).Not(x => x.Class("foo")); + + Assert.Equal(":not(.foo)", target.ToString()); + } + + [Fact] + public void Not_OfType_Matches_Control_Of_Incorrect_Type() + { + var control = new Control1(); + var target = default(Selector).Not(x => x.OfType()); + + Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result); + } + + [Fact] + public void Not_OfType_Doesnt_Match_Control_Of_Correct_Type() + { + var control = new Control2(); + var target = default(Selector).Not(x => x.OfType()); + + Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result); + } + + [Fact] + public async Task Not_Class_Doesnt_Match_Control_With_Class() + { + var control = new Control1 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.False(await match.Activator.Take(1)); + } + + [Fact] + public async Task Not_Class_Matches_Control_Without_Class() + { + var control = new Control1 + { + Classes = new Classes { "bar" }, + }; + + var target = default(Selector).Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.True(await match.Activator.Take(1)); + } + + [Fact] + public async Task OfType_Not_Class_Matches_Control_Without_Class() + { + var control = new Control1 + { + Classes = new Classes { "bar" }, + }; + + var target = default(Selector).OfType().Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.Sometimes, match.Result); + Assert.True(await match.Activator.Take(1)); + } + + [Fact] + public void OfType_Not_Class_Doesnt_Match_Control_Of_Wrong_Type() + { + var control = new Control2 + { + Classes = new Classes { "foo" }, + }; + + var target = default(Selector).OfType().Not(x => x.Class("foo")); + var match = target.Match(control); + + Assert.Equal(SelectorMatchResult.NeverThisType, match.Result); + } + + [Fact] + public void Returns_Correct_TargetType() + { + var target = default(Selector).OfType().Not(x => x.Class("foo")); + + Assert.Equal(typeof(Control1), target.TargetType); + } + + public class Control1 : TestControlBase + { + } + + public class Control2 : TestControlBase + { + } + } +} From c54b9798c7015e79c2997c4ecfc351ff7bdf0b27 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sat, 26 Jan 2019 22:00:38 +0300 Subject: [PATCH 16/46] Use '0000000' for build number formatting and "cibuild" because it's greater than "build" --- nukebuild/BuildParameters.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nukebuild/BuildParameters.cs b/nukebuild/BuildParameters.cs index afd1950859..65ba5e9756 100644 --- a/nukebuild/BuildParameters.cs +++ b/nukebuild/BuildParameters.cs @@ -109,7 +109,7 @@ public partial class Build if (!IsNuGetRelease) { // Use AssemblyVersion with Build as version - Version += "-build" + Environment.GetEnvironmentVariable("BUILD_BUILDID") + "-beta"; + Version += "-cibuild" + int.Parse(Environment.GetEnvironmentVariable("BUILD_BUILDID")).ToString("0000000") + "-beta"; } PublishTestResults = true; From d2e930af3800614f1e33823cb320a42710c06dd5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Sun, 27 Jan 2019 19:14:33 +0300 Subject: [PATCH 17/46] [X11] Use simple DllImport-based GTK file dialog instead of GTK3 backend --- src/Avalonia.X11/Avalonia.X11.csproj | 1 - src/Avalonia.X11/NativeDialogs/Gtk.cs | 263 ++++++++++++++++++ .../NativeDialogs/GtkNativeFileDialogs.cs | 122 ++++++++ src/Avalonia.X11/X11Platform.cs | 4 +- 4 files changed, 387 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.X11/NativeDialogs/Gtk.cs create mode 100644 src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs diff --git a/src/Avalonia.X11/Avalonia.X11.csproj b/src/Avalonia.X11/Avalonia.X11.csproj index 087ba017ae..1629890568 100644 --- a/src/Avalonia.X11/Avalonia.X11.csproj +++ b/src/Avalonia.X11/Avalonia.X11.csproj @@ -7,7 +7,6 @@ - diff --git a/src/Avalonia.X11/NativeDialogs/Gtk.cs b/src/Avalonia.X11/NativeDialogs/Gtk.cs new file mode 100644 index 0000000000..100996984b --- /dev/null +++ b/src/Avalonia.X11/NativeDialogs/Gtk.cs @@ -0,0 +1,263 @@ +using System; +using System.Runtime.InteropServices; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Platform.Interop; +// ReSharper disable IdentifierTypo +namespace Avalonia.X11.NativeDialogs +{ + + static unsafe class Glib + { + private const string GlibName = "libglib-2.0.so.0"; + private const string GObjectName = "libgobject-2.0.so.0"; + + [DllImport(GlibName)] + public static extern void g_slist_free(GSList* data); + + [DllImport(GObjectName)] + private static extern void g_object_ref(IntPtr instance); + + [DllImport(GObjectName)] + private static extern ulong g_signal_connect_object(IntPtr instance, Utf8Buffer signal, + IntPtr handler, IntPtr userData, int flags); + + [DllImport(GObjectName)] + private static extern void g_object_unref(IntPtr instance); + + [DllImport(GObjectName)] + private static extern ulong g_signal_handler_disconnect(IntPtr instance, ulong connectionId); + + private delegate bool timeout_callback(IntPtr data); + + [DllImport(GlibName)] + private static extern ulong g_timeout_add_full(int prio, uint interval, timeout_callback callback, IntPtr data, + IntPtr destroy); + + + class ConnectedSignal : IDisposable + { + private readonly IntPtr _instance; + private GCHandle _handle; + private readonly ulong _id; + + public ConnectedSignal(IntPtr instance, GCHandle handle, ulong id) + { + _instance = instance; + g_object_ref(instance); + _handle = handle; + _id = id; + } + + public void Dispose() + { + if (_handle.IsAllocated) + { + g_signal_handler_disconnect(_instance, _id); + g_object_unref(_instance); + _handle.Free(); + } + } + } + + public static IDisposable ConnectSignal(IntPtr obj, string name, T handler) + { + var handle = GCHandle.Alloc(handler); + var ptr = Marshal.GetFunctionPointerForDelegate((Delegate)(object)handler); + using (var utf = new Utf8Buffer(name)) + { + var id = g_signal_connect_object(obj, utf, ptr, IntPtr.Zero, 0); + if (id == 0) + throw new ArgumentException("Unable to connect to signal " + name); + return new ConnectedSignal(obj, handle, id); + } + } + + + static bool TimeoutHandler(IntPtr data) + { + var handle = GCHandle.FromIntPtr(data); + var cb = (Func)handle.Target; + if (!cb()) + { + handle.Free(); + return false; + } + + return true; + } + + private static readonly timeout_callback s_pinnedHandler; + + static Glib() + { + s_pinnedHandler = TimeoutHandler; + } + + static void AddTimeout(int priority, uint interval, Func callback) + { + var handle = GCHandle.Alloc(callback); + g_timeout_add_full(priority, interval, s_pinnedHandler, GCHandle.ToIntPtr(handle), IntPtr.Zero); + } + + public static Task RunOnGlibThread(Func action) + { + var tcs = new TaskCompletionSource(); + AddTimeout(0, 0, () => + { + + try + { + tcs.SetResult(action()); + } + catch (Exception e) + { + tcs.TrySetException(e); + } + + return false; + }); + return tcs.Task; + } + } + + [StructLayout(LayoutKind.Sequential)] + unsafe struct GSList + { + public readonly IntPtr Data; + public readonly GSList* Next; + } + + enum GtkFileChooserAction + { + Open, + Save, + SelectFolder, + } + + // ReSharper disable UnusedMember.Global + enum GtkResponseType + { + Help = -11, + Apply = -10, + No = -9, + Yes = -8, + Close = -7, + Cancel = -6, + Ok = -5, + DeleteEvent = -4, + Accept = -3, + Reject = -2, + None = -1, + } + // ReSharper restore UnusedMember.Global + + static unsafe class Gtk + { + private static IntPtr s_display; + private const string GdkName = "libgdk-3.so.0"; + private const string GtkName = "libgtk-3.so.0"; + + [DllImport(GtkName)] + static extern void gtk_main_iteration(); + + + [DllImport(GtkName)] + public static extern void gtk_window_set_modal(IntPtr window, bool modal); + + [DllImport(GtkName)] + public static extern void gtk_window_present(IntPtr gtkWindow); + + + public delegate bool signal_generic(IntPtr gtkWidget, IntPtr userData); + + public delegate bool signal_dialog_response(IntPtr gtkWidget, GtkResponseType response, IntPtr userData); + + [DllImport(GtkName)] + public static extern IntPtr gtk_file_chooser_dialog_new(Utf8Buffer title, IntPtr parent, + GtkFileChooserAction action, IntPtr ignore); + + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_select_multiple(IntPtr chooser, bool allow); + + [DllImport(GtkName)] + public static extern void + gtk_dialog_add_button(IntPtr raw, Utf8Buffer button_text, GtkResponseType response_id); + + [DllImport(GtkName)] + public static extern GSList* gtk_file_chooser_get_filenames(IntPtr chooser); + + [DllImport(GtkName)] + public static extern void gtk_file_chooser_set_filename(IntPtr chooser, Utf8Buffer file); + + [DllImport(GtkName)] + public static extern void gtk_widget_realize(IntPtr gtkWidget); + + [DllImport(GtkName)] + public static extern IntPtr gtk_widget_get_window(IntPtr gtkWidget); + + [DllImport(GtkName)] + public static extern void gtk_widget_hide(IntPtr gtkWidget); + + [DllImport(GtkName)] + static extern bool gtk_init_check(int argc, IntPtr argv); + + [DllImport(GdkName)] + static extern IntPtr gdk_x11_window_foreign_new_for_display(IntPtr display, IntPtr xid); + + [DllImport(GdkName)] + static extern IntPtr gdk_set_allowed_backends(Utf8Buffer backends); + + [DllImport(GdkName)] + static extern IntPtr gdk_display_get_default(); + + [DllImport(GtkName)] + static extern IntPtr gtk_application_new(Utf8Buffer appId, int flags); + + [DllImport(GdkName)] + public static extern void gdk_window_set_transient_for(IntPtr window, IntPtr parent); + + public static IntPtr GetForeignWindow(IntPtr xid) => gdk_x11_window_foreign_new_for_display(s_display, xid); + + public static Task StartGtk() + { + var tcs = new TaskCompletionSource(); + new Thread(() => + { + try + { + using (var backends = new Utf8Buffer("x11")) + gdk_set_allowed_backends(backends); + } + catch + { + //Ignore + } + + Environment.SetEnvironmentVariable("WAYLAND_DISPLAY", + "/proc/fake-display-to-prevent-wayland-initialization-by-gtk3"); + + if (!gtk_init_check(0, IntPtr.Zero)) + { + tcs.SetResult(false); + return; + } + + IntPtr app; + using (var utf = new Utf8Buffer($"avalonia.app.a{Guid.NewGuid():N}")) + app = gtk_application_new(utf, 0); + if (app == IntPtr.Zero) + { + tcs.SetResult(false); + return; + } + + s_display = gdk_display_get_default(); + tcs.SetResult(true); + while (true) + gtk_main_iteration(); + }) {Name = "GTK3THREAD", IsBackground = true}.Start(); + return tcs.Task; + } + } +} diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs new file mode 100644 index 0000000000..61047ef2a9 --- /dev/null +++ b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Platform; +using Avalonia.Platform.Interop; +using static Avalonia.X11.NativeDialogs.Glib; +using static Avalonia.X11.NativeDialogs.Gtk; +// ReSharper disable AccessToModifiedClosure +namespace Avalonia.X11.NativeDialogs +{ + class GtkSystemDialog : ISystemDialogImpl + { + private Task _initialized; + private unsafe Task ShowDialog(string title, IWindowImpl parent, GtkFileChooserAction action, + bool multiSelect, string initialFileName) + { + IntPtr dlg; + using (var name = new Utf8Buffer(title)) + dlg = gtk_file_chooser_dialog_new(name, IntPtr.Zero, action, IntPtr.Zero); + UpdateParent(dlg, parent); + if (multiSelect) + gtk_file_chooser_set_select_multiple(dlg, true); + + gtk_window_set_modal(dlg, true); + var tcs = new TaskCompletionSource(); + List disposables = null; + + void Dispose() + { + // ReSharper disable once PossibleNullReferenceException + foreach (var d in disposables) d.Dispose(); + disposables.Clear(); + } + + disposables = new List + { + ConnectSignal(dlg, "close", delegate + { + tcs.TrySetResult(null); + Dispose(); + return false; + }), + ConnectSignal(dlg, "response", (_, resp, __) => + { + string[] result = null; + if (resp == GtkResponseType.Accept) + { + var resultList = new List(); + var gs = gtk_file_chooser_get_filenames(dlg); + var cgs = gs; + while (cgs != null) + { + if (cgs->Data != IntPtr.Zero) + resultList.Add(Utf8Buffer.StringFromPtr(cgs->Data)); + cgs = cgs->Next; + } + g_slist_free(gs); + result = resultList.ToArray(); + } + + gtk_widget_hide(dlg); + Dispose(); + tcs.TrySetResult(result); + return false; + }) + }; + using (var open = new Utf8Buffer("Open")) + gtk_dialog_add_button(dlg, open, GtkResponseType.Accept); + using (var open = new Utf8Buffer("Cancel")) + gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel); + if (initialFileName != null) + using (var fn = new Utf8Buffer(initialFileName)) + gtk_file_chooser_set_filename(dlg, fn); + gtk_window_present(dlg); + return tcs.Task; + } + + public async Task ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) + { + await EnsureInitialized(); + return await await RunOnGlibThread( + () => ShowDialog(dialog.Title, parent, + dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save, + (dialog as OpenFileDialog)?.AllowMultiple ?? false, + Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory, + string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName))); + } + + public async Task ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) + { + await EnsureInitialized(); + return await await RunOnGlibThread(async () => + { + var res = await ShowDialog(dialog.Title, parent, + GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory); + return res?.FirstOrDefault(); + }); + } + + async Task EnsureInitialized() + { + if (_initialized == null) _initialized = StartGtk(); + + if (!(await _initialized)) + throw new Exception("Unable to initialize GTK on separate thread"); + } + + void UpdateParent(IntPtr chooser, IWindowImpl parentWindow) + { + var xid = parentWindow.Handle.Handle; + gtk_widget_realize(chooser); + var window = gtk_widget_get_window(chooser); + var parent = GetForeignWindow(xid); + if (window != IntPtr.Zero && parent != IntPtr.Zero) + gdk_window_set_transient_for(window, parent); + } + } +} diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index 323411e319..5a19187a22 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using Avalonia.Controls; using Avalonia.Controls.Platform; -using Avalonia.Gtk3; using Avalonia.Input; using Avalonia.Input.Platform; using Avalonia.OpenGL; @@ -10,6 +9,7 @@ using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.X11; using Avalonia.X11.Glx; +using Avalonia.X11.NativeDialogs; using static Avalonia.X11.XLib; namespace Avalonia.X11 { @@ -45,7 +45,7 @@ namespace Avalonia.X11 .Bind().ToConstant(new X11Clipboard(this)) .Bind().ToConstant(new PlatformSettingsStub()) .Bind().ToConstant(new X11IconLoader(Info)) - .Bind().ToConstant(new Gtk3ForeignX11SystemDialog()); + .Bind().ToConstant(new GtkSystemDialog()); X11Screens = Avalonia.X11.X11Screens.Init(this); Screens = new X11Screens(X11Screens); From c742fa42d285679adb4fb22bcc6bb00cead39f2a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 29 Jan 2019 18:23:35 +0300 Subject: [PATCH 18/46] [REMOTE] Skip unknown BSON properties --- src/Avalonia.Remote.Protocol/MetsysBson.cs | 7 +- .../TcpTransportBase.cs | 6 +- .../Avalonia.DesignerSupport.Tests/Helpers.cs | 62 +++++++ .../RemoteProtocolTests.cs | 171 ++++++++++++++++++ 4 files changed, 238 insertions(+), 8 deletions(-) create mode 100644 tests/Avalonia.DesignerSupport.Tests/Helpers.cs create mode 100644 tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs diff --git a/src/Avalonia.Remote.Protocol/MetsysBson.cs b/src/Avalonia.Remote.Protocol/MetsysBson.cs index f6bb73129f..925fe10681 100644 --- a/src/Avalonia.Remote.Protocol/MetsysBson.cs +++ b/src/Avalonia.Remote.Protocol/MetsysBson.cs @@ -1190,10 +1190,6 @@ namespace Metsys.Bson object container = null; var property = typeHelper.FindProperty(name); var propertyType = property != null ? property.Type : _typeMap.ContainsKey(storageType) ? _typeMap[storageType] : typeof(object); - if (property == null && typeHelper.Expando == null) - { - throw new BsonException(string.Format("Deserialization failed: type {0} does not have a property named {1}", type.FullName, name)); - } if (property != null && property.Setter == null) { container = property.Getter(instance); @@ -1201,7 +1197,8 @@ namespace Metsys.Bson var value = isNull ? null : DeserializeValue(propertyType, storageType, container, options); if (property == null) { - ((IDictionary)typeHelper.Expando.Getter(instance))[name] = value; + if (typeHelper.Expando != null) + ((IDictionary)typeHelper.Expando.Getter(instance))[name] = value; } else if (container == null && value != null && !property.Ignored) { diff --git a/src/Avalonia.Remote.Protocol/TcpTransportBase.cs b/src/Avalonia.Remote.Protocol/TcpTransportBase.cs index 562dbdf8f9..d01265c9f4 100644 --- a/src/Avalonia.Remote.Protocol/TcpTransportBase.cs +++ b/src/Avalonia.Remote.Protocol/TcpTransportBase.cs @@ -46,7 +46,7 @@ namespace Avalonia.Remote.Protocol { try { - var cl = await server.AcceptTcpClientAsync(); + var cl = await server.AcceptTcpClientAsync().ConfigureAwait(false); AcceptNew(); await Task.Run(async () => { @@ -54,7 +54,7 @@ namespace Avalonia.Remote.Protocol var t = CreateTransport(_resolver, cl.GetStream(), () => tcs.TrySetResult(0)); cb(t); await tcs.Task; - }); + }).ConfigureAwait(false); } catch { @@ -69,7 +69,7 @@ namespace Avalonia.Remote.Protocol public async Task Connect(IPAddress address, int port) { var c = new TcpClient(); - await c.ConnectAsync(address, port); + await c.ConnectAsync(address, port).ConfigureAwait(false); return CreateTransport(_resolver, c.GetStream(), ((IDisposable)c).Dispose); } } diff --git a/tests/Avalonia.DesignerSupport.Tests/Helpers.cs b/tests/Avalonia.DesignerSupport.Tests/Helpers.cs new file mode 100644 index 0000000000..223a86a9af --- /dev/null +++ b/tests/Avalonia.DesignerSupport.Tests/Helpers.cs @@ -0,0 +1,62 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.DesignerSupport.Tests +{ + static class Helpers + { + public static void StructDiff(object parsed, object expected) => StructDiff(parsed, expected, "{root}"); + + static void StructDiff(object parsed, object expected, string path) + { + if (parsed == null && expected == null) + return; + if ((parsed == null && expected != null) || (parsed != null && expected == null)) + throw new Exception( + $"{path}: Null mismatch: {(parsed == null ? "null" : "not-null")} {(expected == null ? "null" : "not-null")}"); + + if (parsed.GetType() != expected.GetType()) + throw new Exception($"{path}: Type mismatch: {parsed.GetType()} {expected.GetType()}"); + + if (parsed is string || parsed.GetType().IsPrimitive) + { + if (!parsed.Equals(expected)) + throw new Exception($"{path}: Not equal {parsed} {expected}"); + } + else if (parsed is IDictionary dic) + { + var dic2 = (IDictionary) expected; + if (dic.Count != dic2.Count) + throw new Exception($"{path}: Dictionary count mismatch: {dic.Count} {dic2.Count}"); + + foreach (var k in dic.Keys.Cast().OrderBy(o => o.ToString())) + { + var v1 = dic[k]; + var v2 = dic2[k]; + StructDiff(v1, v2, path + "['" + k + "']"); + } + } + else if (parsed is IList col) + { + var col2 = (IList) expected; + if (col.Count != col2.Count) + throw new Exception($"{path}: Collection count mismatch: {col.Count} {col2.Count}"); + for (var c = 0; c < col.Count; c++) + StructDiff(col[c], col2[c], path + "[" + c + "]"); + } + else + { + foreach (var prop in parsed.GetType().GetProperties() + .Where(p => p.GetMethod != null && p.GetMethod.IsPublic)) + { + StructDiff(prop.GetValue(parsed), prop.GetValue(expected), path + "." + prop.Name); + } + } + + + + } + } +} diff --git a/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs new file mode 100644 index 0000000000..e5a477cc32 --- /dev/null +++ b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs @@ -0,0 +1,171 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Reflection; +using System.Threading; +using System.Threading.Tasks; +using Avalonia.Remote.Protocol; +using Avalonia.Remote.Protocol.Viewport; +using Xunit; + +namespace Avalonia.DesignerSupport.Tests +{ + public class RemoteProtocolTests : IDisposable + { + private readonly List _disposables = new List(); + private IAvaloniaRemoteTransportConnection _server; + private IAvaloniaRemoteTransportConnection _client; + private BlockingCollection _serverMessages = new BlockingCollection(); + private BlockingCollection _clientMessages = new BlockingCollection(); + private SynchronizationContext _originalContext; + + + class DisabledSyncContext : SynchronizationContext + { + public override void Post(SendOrPostCallback d, object state) + { + throw new InvalidCastException("Not allowed"); + } + + public override void Send(SendOrPostCallback d, object state) + { + throw new InvalidCastException("Not allowed"); + } + } + + void Init(IMessageTypeResolver clientResolver = null, IMessageTypeResolver serverResolver = null) + { + _originalContext = SynchronizationContext.Current; + SynchronizationContext.SetSynchronizationContext(new DisabledSyncContext()); + var clientTransport = new BsonTcpTransport(clientResolver ?? new DefaultMessageTypeResolver()); + var serverTransport = new BsonTcpTransport(serverResolver ?? new DefaultMessageTypeResolver()); + + var tcpListener = new TcpListener(IPAddress.Loopback, 0); + tcpListener.Start(); + var port = ((IPEndPoint)tcpListener.LocalEndpoint).Port; + tcpListener.Stop(); + + var tcs = new TaskCompletionSource(); + serverTransport.Listen(IPAddress.Loopback, port, connected => + { + _server = connected; + tcs.SetResult(0); + }); + _client = clientTransport.Connect(IPAddress.Loopback, port).Result; + _disposables.Add(_client); + _client.OnMessage += (_, m) => _clientMessages.Add(m); + tcs.Task.Wait(); + _disposables.Add(_server); + _server.OnMessage += (_, m) => _serverMessages.Add(m); + + } + + object TakeServer() + { + var src = new CancellationTokenSource(200); + try + { + return _serverMessages.Take(src.Token); + } + finally + { + src.Dispose(); + } + + } + + [Fact] + void EntitiesAreProperlySerializedAndDeserialized() + { + Init(); + var rnd = new Random(); + _server.OnMessage += (_, message) => { }; + + + object GetRandomValue(Type t, string pathInfo) + { + if (t.IsArray) + { + var arr = Array.CreateInstance(t.GetElementType(), 1); + ((IList)arr)[0] = GetRandomValue(t.GetElementType(), pathInfo); + return arr; + } + + if (t == typeof(bool)) + return true; + if (t == typeof(int) || t == typeof(long)) + return rnd.Next(); + if (t == typeof(byte)) + return (byte)rnd.Next(255); + if (t == typeof(double)) + return rnd.NextDouble(); + if (t.IsEnum) + return ((IList)Enum.GetValues(t)).Cast().Last(); + if (t == typeof(string)) + return Guid.NewGuid().ToString(); + if (t == typeof(Guid)) + return Guid.NewGuid(); + throw new Exception($"Doesn't know how to fabricate a random value for {t}, path {pathInfo}"); + } + + foreach (var t in typeof(MeasureViewportMessage).Assembly.GetTypes().Where(t => + t.GetCustomAttribute(typeof(AvaloniaRemoteMessageGuidAttribute)) != null)) + { + var o = Activator.CreateInstance(t); + foreach (var p in t.GetProperties()) + p.SetValue(o, GetRandomValue(p.PropertyType, $"{t.FullName}.{p.Name}")); + + _client.Send(o).Wait(200); + var received = TakeServer(); + Helpers.StructDiff(received, o); + + } + + + } + + [Fact] + void RemoteProtocolShouldBeBackwardsCompatible() + { + Init(new DefaultMessageTypeResolver(typeof(ExtendedMeasureViewportMessage).Assembly)); + _client.Send(new ExtendedMeasureViewportMessage() + { + Width = 100, Height = 200, SomeNewProperty = 300, + SomeArrayProperty = new[]{1,2,3}, + SubObjectProperty = new ExtendedMeasureViewportMessage.SubObject() + { + Foo = 543 + } + }); + var received = (MeasureViewportMessage)TakeServer(); + Assert.Equal(100, received.Width); + Assert.Equal(200, received.Height); + + } + + public void Dispose() + { + _disposables.ForEach(d => d.Dispose()); + SynchronizationContext.SetSynchronizationContext(_originalContext); + } + } + + [AvaloniaRemoteMessageGuid("6E3C5310-E2B1-4C3D-8688-01183AA48C5B")] + public class ExtendedMeasureViewportMessage + { + public double Width { get; set; } + + public int SomeNewProperty { get; set; } + public int[] SomeArrayProperty { get; set; } + public class SubObject + { + public int Foo { get; set; } + } + public SubObject SubObjectProperty { get; set; } + public double Height { get; set; } + } +} From c99f70f4e4e94952140ef5a5b673576a50e42d77 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 29 Jan 2019 20:15:45 +0000 Subject: [PATCH 19/46] fix replace script. --- scripts/ReplaceNugetCache.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index 6de50f978d..b46564c3fd 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -1,6 +1,4 @@ -copy ..\samples\ControlCatalog.Desktop\bin\Debug\net461\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\net461\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Gtk3.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ From a2d26abdf257ce83d0d694e1ea510316f96c5ef8 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 29 Jan 2019 20:30:05 +0000 Subject: [PATCH 20/46] replace script also replaces direct2d --- scripts/ReplaceNugetCache.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index b46564c3fd..70f5eaa40b 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -2,3 +2,4 @@ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\ From 9d3fd84a7acd5c718aa628d3c126912745cf31b7 Mon Sep 17 00:00:00 2001 From: mstr2 Date: Tue, 29 Jan 2019 01:16:26 +0100 Subject: [PATCH 21/46] Fixes a bug where properties would be added multiple times to the global AvaloniaPropertyRegistry._properties list. --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 10 +++++++--- .../AvaloniaPropertyRegistryTests.cs | 12 ++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 11b1096052..6f57dfbf13 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -13,7 +13,7 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { - private readonly IList _properties = + private readonly List _properties = new List(); private readonly Dictionary> _registered = new Dictionary>(); @@ -30,6 +30,11 @@ namespace Avalonia public static AvaloniaPropertyRegistry Instance { get; } = new AvaloniaPropertyRegistry(); + /// + /// Gets a list of all registered properties. + /// + internal IReadOnlyList Properties => _properties; + /// /// Gets all non-attached s registered on a type. /// @@ -250,8 +255,7 @@ namespace Avalonia { inner.Add(property.Id, property); } - - _properties.Add(property); + _attachedCache.Clear(); } } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs index c34e26ac5c..8220b7d6e7 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs @@ -19,6 +19,18 @@ namespace Avalonia.Base.UnitTests p = AttachedOwner.AttachedProperty; } + [Fact] + public void Registered_Properties_Count_Reflects_Newly_Added_Attached_Property() + { + var registry = new AvaloniaPropertyRegistry(); + var metadata = new StyledPropertyMetadata(); + var property = new AttachedProperty("test", typeof(object), metadata, true); + registry.Register(typeof(object), property); + registry.RegisterAttached(typeof(AvaloniaPropertyRegistryTests), property); + + Assert.Equal(1, registry.Properties.Count); + } + [Fact] public void GetRegistered_Returns_Registered_Properties() { From 4d73f1d159b96ae004848903351e7cb8ca3cc45d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Feb 2019 10:33:08 +0100 Subject: [PATCH 22/46] Fix deadlock in remote protocol. The `TransportConnectionWrapper` producer-consumer queue was deadlocking due to `_signal` getting set to `null` while a worker was still waiting for it. Spoke with @kekekeks who suggested this fix. --- src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs b/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs index 1e821b7c24..d7919af9d9 100644 --- a/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs +++ b/src/Avalonia.Remote.Protocol/TransportConnectionWrapper.cs @@ -64,7 +64,7 @@ namespace Avalonia.Remote.Protocol public Task Send(object data) { - var tcs = new TaskCompletionSource(); + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); lock (_lock) { if (!_workerIsAlive) @@ -79,8 +79,9 @@ namespace Avalonia.Remote.Protocol }); if (_signal != null) { - _signal.SetResult(0); + var signal = _signal; _signal = null; + signal.SetResult(0); } } return tcs.Task; @@ -98,4 +99,4 @@ namespace Avalonia.Remote.Protocol remove => _onException.Remove(value); } } -} \ No newline at end of file +} From 8ee278dd807a029c6320e87d556e63a6a756fad3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Feb 2019 11:59:53 +0100 Subject: [PATCH 23/46] Update portable.xaml to latest upstream master. --- .../PortableXaml/AvaloniaXamlSchemaContext.cs | 2 +- .../Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs index 2d6e046f51..9a493a85c0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs @@ -33,7 +33,7 @@ namespace Avalonia.Markup.Xaml.PortableXaml private IRuntimeTypeProvider _avaloniaTypeProvider; - protected internal override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments) + protected override XamlType GetXamlType(string xamlNamespace, string name, params XamlType[] typeArguments) { XamlType type = null; try diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index 8abbe09592..ab55261737 160000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github @@ -1 +1 @@ -Subproject commit 8abbe09592668efb573ac4d5548ba2d7e464ba78 +Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975 From 2f7c6371a546998b1aabbdad08062178027c6c8c Mon Sep 17 00:00:00 2001 From: ahopper Date: Sat, 2 Feb 2019 18:34:11 +0000 Subject: [PATCH 24/46] use XDocument to deserialize assets --- .../Utilities/AvaloniaResourcesIndex.cs | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs index 22e5c952bf..66024236da 100644 --- a/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs +++ b/src/Avalonia.Base/Utilities/AvaloniaResourcesIndex.cs @@ -4,6 +4,8 @@ using System.IO; using System.Runtime.CompilerServices; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; +using System.Xml.Linq; +using System.Linq; // ReSharper disable AssignNullToNotNullAttribute @@ -19,10 +21,20 @@ namespace Avalonia.Utilities { var ver = new BinaryReader(stream).ReadInt32(); if (ver > LastKnownVersion) - throw new Exception("Resources index format version is not known"); - var index = (AvaloniaResourcesIndex) - new DataContractSerializer(typeof(AvaloniaResourcesIndex)).ReadObject(stream); - return index.Entries; + throw new Exception("Resources index format version is not known"); + + var assetDoc = XDocument.Load(stream); + XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value; + List entries= + (from entry in assetDoc.Root.Element(assetNs + "Entries").Elements(assetNs + "AvaloniaResourcesIndexEntry") + select new AvaloniaResourcesIndexEntry + { + Path = entry.Element(assetNs + "Path").Value, + Offset = int.Parse(entry.Element(assetNs + "Offset").Value), + Size = int.Parse(entry.Element(assetNs + "Size").Value) + }).ToList(); + + return entries; } public static void Write(Stream stream, List entries) From ce3461ac46affe7bff909cceca58c4d6365ed28f Mon Sep 17 00:00:00 2001 From: ahopper Date: Sun, 3 Feb 2019 07:29:49 +0000 Subject: [PATCH 25/46] use xdocument in XamlLoader --- .../Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index a825deeae3..255357e027 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -13,6 +13,8 @@ using System.Reflection; using System.Runtime.Serialization; using System.Runtime.Serialization.Json; using System.Text; +using System.Xml.Linq; +using System.Linq; namespace Avalonia.Markup.Xaml { @@ -240,15 +242,21 @@ namespace Avalonia.Markup.Xaml { using (var xamlInfoStream = assetLocator.Open(xamlInfoUri)) { - var xamlInfo = (AvaloniaResourceXamlInfo)s_xamlInfoSerializer.ReadObject(xamlInfoStream); - if (xamlInfo.ClassToResourcePathIndex.TryGetValue(typeName, out var rv) == true) + var assetDoc = XDocument.Load(xamlInfoStream); + XNamespace assetNs = assetDoc.Root.Attribute("xmlns").Value; + XNamespace arrayNs = "http://schemas.microsoft.com/2003/10/Serialization/Arrays"; + Dictionary xamlInfo = + assetDoc.Root.Element(assetNs + "ClassToResourcePathIndex").Elements(arrayNs + "KeyValueOfstringstring") + .ToDictionary(entry =>entry.Element(arrayNs + "Key").Value, + entry => entry.Element(arrayNs + "Value").Value); + + if (xamlInfo.TryGetValue(typeName, out var rv) == true) { yield return new Uri($"avares://{asm}{rv}"); yield break; } } - } - + } yield return new Uri("resm:" + typeName + ".xaml?assembly=" + asm); yield return new Uri("resm:" + typeName + ".paml?assembly=" + asm); From a24c29175624c06ed58664056a5d411132ddec52 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 7 Feb 2019 15:03:10 +0000 Subject: [PATCH 26/46] potential fix for nre in deferred renderer. --- .../Rendering/DeferredRenderer.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index 60e624948e..f9c21bd212 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -255,15 +255,19 @@ namespace Avalonia.Rendering } var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext); - using (scene) + + if (scene != null) { - var overlay = DrawDirtyRects || DrawFps; - if (DrawDirtyRects) - _dirtyRectsDisplay.Tick(); - if (overlay) - RenderOverlay(scene.Item, GetContext()); - if (updated || forceComposite || overlay) - RenderComposite(scene.Item, GetContext()); + using (scene) + { + var overlay = DrawDirtyRects || DrawFps; + if (DrawDirtyRects) + _dirtyRectsDisplay.Tick(); + if (overlay) + RenderOverlay(scene.Item, GetContext()); + if (updated || forceComposite || overlay) + RenderComposite(scene.Item, GetContext()); + } } } finally From af83673f918373f904f91b7abc43c683669602ea Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 7 Feb 2019 16:46:59 +0200 Subject: [PATCH 27/46] make DropDown support mouse wheel --- src/Avalonia.Controls/DropDown.cs | 53 ++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 93b33e0589..684279a3cd 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -56,6 +56,7 @@ namespace Avalonia.Controls private bool _isDropDownOpen; private Popup _popup; private object _selectionBoxItem; + private IDisposable _subscriptionsOnOpen; /// /// Initializes static members of the class. @@ -174,6 +175,37 @@ namespace Avalonia.Controls } } + /// + protected override void OnPointerWheelChanged(PointerWheelEventArgs e) + { + base.OnPointerWheelChanged(e); + + if (!e.Handled) + { + if (!IsDropDownOpen) + { + if (IsFocused) + { + if (e.Delta.Y < 0) + { + if (++SelectedIndex >= ItemCount) + SelectedIndex = 0; + } + else + { + if (--SelectedIndex < 0) + SelectedIndex = ItemCount - 1; + } + e.Handled = true; + } + } + else + { + e.Handled = true; + } + } + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { @@ -223,6 +255,9 @@ namespace Avalonia.Controls private void PopupClosed(object sender, EventArgs e) { + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + if (CanFocus(this)) { Focus(); @@ -232,6 +267,22 @@ namespace Avalonia.Controls private void PopupOpened(object sender, EventArgs e) { TryFocusSelectedItem(); + + _subscriptionsOnOpen?.Dispose(); + _subscriptionsOnOpen = null; + + var toplevel = this.GetVisualRoot() as TopLevel; + if (toplevel != null) + { + _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) => + { + //eat wheel scroll event outside dropdown popup while it's open + if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel) + { + ev.Handled = true; + } + }, Interactivity.RoutingStrategies.Tunnel); + } } private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e) @@ -247,7 +298,7 @@ namespace Avalonia.Controls { var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); - if(container == null && SelectedItems.Count > 0) + if (container == null && SelectedItems.Count > 0) { ScrollIntoView(SelectedItems[0]); container = ItemContainerGenerator.ContainerFromIndex(selectedIndex); From f285c38d2766368767f2fee844743fd7365af821 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Thu, 7 Feb 2019 17:22:56 +0200 Subject: [PATCH 28/46] improve dropdown key up/down and mouse wheel auto selections so they don't go through unselected state --- src/Avalonia.Controls/DropDown.cs | 39 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index 684279a3cd..4f65a0aed4 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/src/Avalonia.Controls/DropDown.cs @@ -150,16 +150,12 @@ namespace Avalonia.Controls { if (e.Key == Key.Down) { - if (++SelectedIndex >= ItemCount) - SelectedIndex = 0; - + SelectNext(); e.Handled = true; } else if (e.Key == Key.Up) { - if (--SelectedIndex < 0) - SelectedIndex = ItemCount - 1; - + SelectPrev(); e.Handled = true; } } @@ -187,15 +183,10 @@ namespace Avalonia.Controls if (IsFocused) { if (e.Delta.Y < 0) - { - if (++SelectedIndex >= ItemCount) - SelectedIndex = 0; - } + SelectNext(); else - { - if (--SelectedIndex < 0) - SelectedIndex = ItemCount - 1; - } + SelectPrev(); + e.Handled = true; } } @@ -358,5 +349,25 @@ namespace Avalonia.Controls } } } + + private void SelectNext() + { + int next = SelectedIndex + 1; + + if (next >= ItemCount) + next = 0; + + SelectedIndex = next; + } + + private void SelectPrev() + { + int prev = SelectedIndex - 1; + + if (prev < 0) + prev = ItemCount - 1; + + SelectedIndex = prev; + } } } From a8b8454a6c680ca9bedcf93caba6de15540da0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=97=D0=B0?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=D1=81=D0=BA=D0=BE=D0=B9?= Date: Sat, 9 Feb 2019 03:54:28 +0300 Subject: [PATCH 29/46] add StrokeDashOffset support for shapes --- src/Avalonia.Controls/Shapes/Shape.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index f77c43acd0..0387328a46 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -20,7 +20,10 @@ namespace Avalonia.Controls.Shapes AvaloniaProperty.Register(nameof(Stroke)); public static readonly StyledProperty> StrokeDashArrayProperty = - AvaloniaProperty.Register>("StrokeDashArray"); + AvaloniaProperty.Register>(nameof(StrokeDashArray)); + + public static readonly StyledProperty StrokeDashOffsetProperty = + AvaloniaProperty.Register(nameof(StrokeDashOffset)); public static readonly StyledProperty StrokeThicknessProperty = AvaloniaProperty.Register(nameof(StrokeThickness)); @@ -103,6 +106,12 @@ namespace Avalonia.Controls.Shapes get { return GetValue(StrokeDashArrayProperty); } set { SetValue(StrokeDashArrayProperty, value); } } + + public double StrokeDashOffset + { + get { return GetValue(StrokeDashOffsetProperty); } + set { SetValue(StrokeDashOffsetProperty, value); } + } public double StrokeThickness { @@ -124,7 +133,7 @@ namespace Avalonia.Controls.Shapes if (geometry != null) { - var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray), + var pen = new Pen(Stroke, StrokeThickness, new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeDashCap, StrokeStartLineCap, StrokeEndLineCap, StrokeJoin); context.DrawGeometry(Fill, pen, geometry); } From b2f250acb4001a1ad96da1a0315fe989bb914737 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Sat, 9 Feb 2019 17:19:26 +0000 Subject: [PATCH 30/46] Oops --- src/Avalonia.Visuals/Rendering/DeferredRenderer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index f9c21bd212..eb1c1d7471 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -256,7 +256,7 @@ namespace Avalonia.Rendering var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext); - if (scene != null) + if (scene?.Item != null) { using (scene) { From 44cc084bd6cd1d6bbe1259c890dad916aaa5ecd6 Mon Sep 17 00:00:00 2001 From: Sorien Date: Sat, 9 Feb 2019 19:08:53 +0100 Subject: [PATCH 31/46] Update PR template hide section desc with html comments --- .github/PULL_REQUEST_TEMPLATE.md | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 78b9cff039..acff8cc117 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,18 +1,18 @@ ## What does the pull request do? + -Give a bit of background on the PR here, together with links to with related issues etc. ## What is the current behavior? + -If the PR is a fix, describe the current incorrect behavior, otherwise delete this section. ## What is the updated/expected behavior with this PR? + -Describe how to test the PR. ## How was the solution implemented (if it's not obvious)? + -Include any information that might be of use to a reviewer here. ## Checklist @@ -21,12 +21,11 @@ Include any information that might be of use to a reviewer here. - [ ] Consider submitting a PR to https://github.com/AvaloniaUI/Avaloniaui.net with user documentation ## Breaking changes + -List any breaking changes here. When the PR is merged please add an entry to https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes ## Fixed issues - -If the pull request fixes issue(s) list them like this: - + From e9f59a90e8aec0ab2ebd321e699ab7edeee92f10 Mon Sep 17 00:00:00 2001 From: danwalmsley Date: Sun, 10 Feb 2019 10:47:05 +0000 Subject: [PATCH 32/46] ensure scene is always disposed --- src/Avalonia.Visuals/Rendering/DeferredRenderer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index eb1c1d7471..3bc5e92fb4 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -256,9 +256,9 @@ namespace Avalonia.Rendering var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(GetContext); - if (scene?.Item != null) + using (scene) { - using (scene) + if (scene?.Item != null) { var overlay = DrawDirtyRects || DrawFps; if (DrawDirtyRects) @@ -267,7 +267,7 @@ namespace Avalonia.Rendering RenderOverlay(scene.Item, GetContext()); if (updated || forceComposite || overlay) RenderComposite(scene.Item, GetContext()); - } + } } } finally From 990bc26d7b42c9437b128c0955cc79639a36f4d9 Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 12:42:53 +0300 Subject: [PATCH 33/46] Add RoutedViewHost control implementation --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 157 ++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 src/Avalonia.ReactiveUI/RoutedViewHost.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs new file mode 100644 index 0000000000..8006ef9aa6 --- /dev/null +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -0,0 +1,157 @@ +using System; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using Avalonia.Animation; +using Avalonia.Controls; +using Avalonia.Styling; +using ReactiveUI; +using Splat; + +namespace Avalonia +{ + /// + /// This control hosts the View associated with a Router, and will display + /// the View and wire up the ViewModel whenever a new ViewModel is navigated to. + /// + public class RoutedViewHost : UserControl, IActivatable, IEnableLogger + { + /// + /// The router dependency property. + /// + public static readonly AvaloniaProperty RouterProperty = + AvaloniaProperty.Register(nameof(Router)); + + /// + /// The default content property. + /// + public static readonly AvaloniaProperty DefaultContentProperty = + AvaloniaProperty.Register(nameof(DefaultContent)); + + private readonly IAnimation _fadeOutAnimation = CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)); + private readonly IAnimation _fadeInAnimation = CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)); + + /// + /// Initializes a new instance of the class. + /// + public RoutedViewHost() + { + this.WhenActivated(disposables => + { + this.WhenAnyObservable(x => x.Router.CurrentViewModel) + .DistinctUntilChanged() + .Subscribe(HandleViewModelChange) + .DisposeWith(disposables); + }); + } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator ViewLocator { get; set; } + + /// + /// Gets or sets the of the view model stack. + /// + public RoutingState Router + { + get => GetValue(RouterProperty); + set => SetValue(RouterProperty, value); + } + + /// + /// Gets or sets the content displayed whenever there is no page currently routed. + /// + public object DefaultContent + { + get => GetValue(DefaultContentProperty); + set => SetValue(DefaultContentProperty, value); + } + + /// + /// Duplicates the Content property with a private setter. + /// + public new object Content + { + get => base.Content; + private set => base.Content = value; + } + + /// + /// Invoked when ReactiveUI router navigates to a view model. + /// + /// ViewModel to which the user navigates. + /// + /// Thrown when ViewLocator is unable to find the appropriate view. + /// + private void HandleViewModelChange(IRoutableViewModel viewModel) + { + if (viewModel == null) + { + this.Log().Info("ViewModel is null, falling back to default content."); + UpdateContent(DefaultContent); + return; + } + + var viewLocator = ViewLocator ?? ReactiveUI.ViewLocator.Current; + var view = viewLocator.ResolveView(viewModel); + if (view == null) throw new Exception($"Couldn't find view for '{viewModel}'. Is it registered?"); + + this.Log().Info($"Ready to show {view} with autowired {viewModel}."); + view.ViewModel = viewModel; + UpdateContent(view); + } + + /// + /// Updates the content with transitions. + /// + /// New content to set. + private async void UpdateContent(object newContent) + { + await _fadeOutAnimation.RunAsync(this, null); + Content = newContent; + await _fadeInAnimation.RunAsync(this, null); + } + + /// + /// Creates opacity animation for this routed view host. + /// + /// Opacity to start from. + /// Opacity to finish with. + /// Duration of the animation. + /// Animation object instance. + private static IAnimation CreateOpacityAnimation(double from, double to, TimeSpan duration) + { + return new Avalonia.Animation.Animation + { + Duration = duration, + Children = + { + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = from + } + }, + Cue = new Cue(0d) + }, + new KeyFrame + { + Setters = + { + new Setter + { + Property = OpacityProperty, + Value = to + } + }, + Cue = new Cue(1d) + } + } + }; + } + } +} From 6e4b9a23c248a33961a501ef614a753c4b9b805f Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 16:28:22 +0300 Subject: [PATCH 34/46] Add unit tests for RoutedViewHost --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 59 +++++++--- .../RoutedViewHostTest.cs | 104 ++++++++++++++++++ 2 files changed, 149 insertions(+), 14 deletions(-) create mode 100644 tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 8006ef9aa6..3613d3d259 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -26,9 +26,20 @@ namespace Avalonia /// public static readonly AvaloniaProperty DefaultContentProperty = AvaloniaProperty.Register(nameof(DefaultContent)); - - private readonly IAnimation _fadeOutAnimation = CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25)); - private readonly IAnimation _fadeInAnimation = CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25)); + + /// + /// Fade in animation property. + /// + public static readonly AvaloniaProperty FadeInAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(0d, 1d, TimeSpan.FromSeconds(0.25))); + + /// + /// Fade out animation property. + /// + public static readonly AvaloniaProperty FadeOutAnimationProperty = + AvaloniaProperty.Register(nameof(DefaultContent), + CreateOpacityAnimation(1d, 0d, TimeSpan.FromSeconds(0.25))); /// /// Initializes a new instance of the class. @@ -39,16 +50,11 @@ namespace Avalonia { this.WhenAnyObservable(x => x.Router.CurrentViewModel) .DistinctUntilChanged() - .Subscribe(HandleViewModelChange) + .Subscribe(NavigateToViewModel) .DisposeWith(disposables); }); } - /// - /// Gets or sets the ReactiveUI view locator used by this router. - /// - public IViewLocator ViewLocator { get; set; } - /// /// Gets or sets the of the view model stack. /// @@ -66,15 +72,38 @@ namespace Avalonia get => GetValue(DefaultContentProperty); set => SetValue(DefaultContentProperty, value); } + + /// + /// Gets or sets the animation played when page appears. + /// + public IAnimation FadeInAnimation + { + get => GetValue(FadeInAnimationProperty); + set => SetValue(FadeInAnimationProperty, value); + } + + /// + /// Gets or sets the animation played when page disappears. + /// + public IAnimation FadeOutAnimation + { + get => GetValue(FadeOutAnimationProperty); + set => SetValue(FadeOutAnimationProperty, value); + } /// /// Duplicates the Content property with a private setter. /// public new object Content { - get => base.Content; + get => base.Content ?? DefaultContent; private set => base.Content = value; } + + /// + /// Gets or sets the ReactiveUI view locator used by this router. + /// + public IViewLocator ViewLocator { get; set; } /// /// Invoked when ReactiveUI router navigates to a view model. @@ -83,12 +112,12 @@ namespace Avalonia /// /// Thrown when ViewLocator is unable to find the appropriate view. /// - private void HandleViewModelChange(IRoutableViewModel viewModel) + private void NavigateToViewModel(IRoutableViewModel viewModel) { if (viewModel == null) { this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(DefaultContent); + UpdateContent(null); return; } @@ -107,9 +136,11 @@ namespace Avalonia /// New content to set. private async void UpdateContent(object newContent) { - await _fadeOutAnimation.RunAsync(this, null); + if (FadeOutAnimation != null) + await FadeOutAnimation.RunAsync(this, Clock); Content = newContent; - await _fadeInAnimation.RunAsync(this, null); + if (FadeInAnimation != null) + await FadeInAnimation.RunAsync(this, Clock); } /// diff --git a/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs new file mode 100644 index 0000000000..c85b999af2 --- /dev/null +++ b/tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs @@ -0,0 +1,104 @@ +using System; +using System.Reactive.Concurrency; +using System.Reactive.Disposables; +using Avalonia.Controls; +using Avalonia.Rendering; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Avalonia; +using ReactiveUI; +using DynamicData; +using Xunit; +using Splat; +using Avalonia.Markup.Xaml; +using System.ComponentModel; +using System.Threading.Tasks; +using System.Reactive; + +namespace Avalonia +{ + public class RoutedViewHostTest + { + public class FirstRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "first"; + + public IScreen HostScreen { get; set; } + } + + public class FirstRoutableView : ReactiveUserControl { } + + public class SecondRoutableViewModel : ReactiveObject, IRoutableViewModel + { + public string UrlPathSegment => "second"; + + public IScreen HostScreen { get; set; } + } + + public class SecondRoutableView : ReactiveUserControl { } + + public class ScreenViewModel : ReactiveObject, IScreen + { + public RoutingState Router { get; } = new RoutingState(); + } + + public RoutedViewHostTest() + { + Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); + Locator.CurrentMutable.Register(() => new FirstRoutableView(), typeof(IViewFor)); + Locator.CurrentMutable.Register(() => new SecondRoutableView(), typeof(IViewFor)); + } + + [Fact] + public void RoutedViewHostShouldStayInSyncWithRoutingState() + { + var screen = new ScreenViewModel(); + var defaultContent = new TextBlock(); + var host = new RoutedViewHost + { + Router = screen.Router, + DefaultContent = defaultContent, + FadeOutAnimation = null, + FadeInAnimation = null + }; + + var root = new TestRoot + { + Child = host + }; + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + + screen.Router.Navigate + .Execute(new FirstRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.Navigate + .Execute(new SecondRoutableViewModel()) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(SecondRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(FirstRoutableView), host.Content.GetType()); + + screen.Router.NavigateBack + .Execute(Unit.Default) + .Subscribe(); + + Assert.NotNull(host.Content); + Assert.Equal(typeof(TextBlock), host.Content.GetType()); + Assert.Equal(defaultContent, host.Content); + } + } +} \ No newline at end of file From 724ec9b8bb0ee923632e7e43d764a957c6f7543e Mon Sep 17 00:00:00 2001 From: artyom Date: Sun, 10 Feb 2019 18:21:51 +0300 Subject: [PATCH 35/46] Add xml docs and usage examples --- src/Avalonia.ReactiveUI/RoutedViewHost.cs | 44 ++++++++++++++++++++--- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs index 3613d3d259..726e086d9c 100644 --- a/src/Avalonia.ReactiveUI/RoutedViewHost.cs +++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs @@ -10,9 +10,45 @@ using Splat; namespace Avalonia { /// - /// This control hosts the View associated with a Router, and will display - /// the View and wire up the ViewModel whenever a new ViewModel is navigated to. + /// This control hosts the View associated with ReactiveUI RoutingState, + /// and will display the View and wire up the ViewModel whenever a new + /// ViewModel is navigated to. Nested routing is also supported. /// + /// + /// + /// ReactiveUI routing consists of an IScreen that contains current + /// RoutingState, several IRoutableViewModels, and a platform-specific + /// XAML control called RoutedViewHost. + /// + /// + /// RoutingState manages the ViewModel navigation stack and allows + /// ViewModels to navigate to other ViewModels. IScreen is the root of + /// a navigation stack; despite the name, its views don't have to occupy + /// the whole screen. RoutedViewHost monitors an instance of RoutingState, + /// responding to any changes in the navigation stack by creating and + /// embedding the appropriate view. + /// + /// + /// Place this control to a view containing your ViewModel that implements + /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property. + /// + /// + /// + /// + /// + /// + /// ]]> + /// + /// + /// + /// See + /// ReactiveUI routing documentation website for more info. + /// + /// public class RoutedViewHost : UserControl, IActivatable, IEnableLogger { /// @@ -96,7 +132,7 @@ namespace Avalonia /// public new object Content { - get => base.Content ?? DefaultContent; + get => base.Content; private set => base.Content = value; } @@ -117,7 +153,7 @@ namespace Avalonia if (viewModel == null) { this.Log().Info("ViewModel is null, falling back to default content."); - UpdateContent(null); + UpdateContent(DefaultContent); return; } From fa8d8c896dbb722bbebdaf484de232fdeca0fdeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Mon, 11 Feb 2019 22:44:09 +0000 Subject: [PATCH 36/46] Removed redundant bool comparisons. --- src/Avalonia.Base/Data/Core/BindingExpression.cs | 2 +- src/Avalonia.Controls/AutoCompleteBox.cs | 2 +- src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs | 2 +- src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs | 2 +- src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Base/Data/Core/BindingExpression.cs b/src/Avalonia.Base/Data/Core/BindingExpression.cs index c4ffa839e0..f1717bde3b 100644 --- a/src/Avalonia.Base/Data/Core/BindingExpression.cs +++ b/src/Avalonia.Base/Data/Core/BindingExpression.cs @@ -177,7 +177,7 @@ namespace Avalonia.Data.Core protected override void Subscribed(IObserver observer, bool first) { - if (!first && _value != null && _value.TryGetTarget(out var val) == true) + if (!first && _value != null && _value.TryGetTarget(out var val)) { observer.OnNext(val); } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index 1bc402bc2f..b054804c86 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1893,7 +1893,7 @@ namespace Avalonia.Controls { bool callTextChanged = false; // Update the Text dependency property - if ((userInitiated == null || userInitiated == true) && Text != value) + if ((userInitiated ?? true) && Text != value) { _ignoreTextPropertyChange++; Text = value; diff --git a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs index c05c1672f8..30330ef9ac 100644 --- a/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs @@ -285,7 +285,7 @@ namespace Avalonia.Controls.Presenters { scrollable.InvalidateScroll = () => UpdateFromScrollable(scrollable); - if (scrollable.IsLogicalScrollEnabled == true) + if (scrollable.IsLogicalScrollEnabled) { _logicalScrollSubscription = new CompositeDisposable( this.GetObservable(CanHorizontallyScrollProperty) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 159c3cd0fa..2fb8e84a2e 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -236,7 +236,7 @@ namespace Avalonia.Rendering.SceneGraph { foreach (var operation in DrawOperations) { - if (operation.Item.HitTest(p) == true) + if (operation.Item.HitTest(p)) { return true; } diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 255357e027..2720e674cc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -250,7 +250,7 @@ namespace Avalonia.Markup.Xaml .ToDictionary(entry =>entry.Element(arrayNs + "Key").Value, entry => entry.Element(arrayNs + "Value").Value); - if (xamlInfo.TryGetValue(typeName, out var rv) == true) + if (xamlInfo.TryGetValue(typeName, out var rv)) { yield return new Uri($"avares://{asm}{rv}"); yield break; From 91e678baf858054b39ee716c0a6a4d17f23f3bb3 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 12 Feb 2019 11:19:06 +0200 Subject: [PATCH 37/46] call canexecute before execute for button/menu command --- src/Avalonia.Controls/Button.cs | 2 +- src/Avalonia.Controls/MenuItem.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index d485924885..f572c67284 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -217,7 +217,7 @@ namespace Avalonia.Controls var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 055d49fb0b..aa20ee0595 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -287,7 +287,7 @@ namespace Avalonia.Controls /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { - if (Command != null) + if (!e.Handled && Command?.CanExecute(CommandParameter) == true) { Command.Execute(CommandParameter); e.Handled = true; From 039991da684187c5d64104ca310a14a6793b496f Mon Sep 17 00:00:00 2001 From: mstr2 Date: Wed, 13 Feb 2019 03:54:04 +0100 Subject: [PATCH 38/46] Fixed a bug where AddOwner would add a property to AvaloniaPropertyRegistry's property list more than once --- src/Avalonia.Base/AvaloniaPropertyRegistry.cs | 12 ++++++++---- .../AvaloniaPropertyRegistryTests.cs | 5 +++++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index 6f57dfbf13..5fcdf76c0f 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -13,8 +13,8 @@ namespace Avalonia /// public class AvaloniaPropertyRegistry { - private readonly List _properties = - new List(); + private readonly Dictionary _properties = + new Dictionary(); private readonly Dictionary> _registered = new Dictionary>(); private readonly Dictionary> _attached = @@ -33,7 +33,7 @@ namespace Avalonia /// /// Gets a list of all registered properties. /// - internal IReadOnlyList Properties => _properties; + internal IReadOnlyCollection Properties => _properties.Values; /// /// Gets all non-attached s registered on a type. @@ -220,7 +220,11 @@ namespace Avalonia inner.Add(property.Id, property); } - _properties.Add(property); + if (!_properties.ContainsKey(property.Id)) + { + _properties.Add(property.Id, property); + } + _registeredCache.Clear(); } diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs index 8220b7d6e7..d11319114f 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaPropertyRegistryTests.cs @@ -27,6 +27,7 @@ namespace Avalonia.Base.UnitTests var property = new AttachedProperty("test", typeof(object), metadata, true); registry.Register(typeof(object), property); registry.RegisterAttached(typeof(AvaloniaPropertyRegistryTests), property); + property.AddOwner(); Assert.Equal(1, registry.Properties.Count); } @@ -150,5 +151,9 @@ namespace Avalonia.Base.UnitTests private class AttachedOwner2 : AttachedOwner { } + + private class Class4 : AvaloniaObject + { + } } } From 0fce33e43ea0c1dd3d2646ea3dda51acc821b676 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Wed, 13 Feb 2019 23:37:39 +0000 Subject: [PATCH 39/46] XML comment fixes. --- src/Avalonia.Controls/MenuItem.cs | 2 +- src/Avalonia.Controls/PixelPointEventArgs.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs index 055d49fb0b..99e00ce72e 100644 --- a/src/Avalonia.Controls/MenuItem.cs +++ b/src/Avalonia.Controls/MenuItem.cs @@ -421,7 +421,7 @@ namespace Avalonia.Controls } /// - /// Called when the property changes. + /// Called when the property changes. /// /// The property change event. private void HeaderChanged(AvaloniaPropertyChangedEventArgs e) diff --git a/src/Avalonia.Controls/PixelPointEventArgs.cs b/src/Avalonia.Controls/PixelPointEventArgs.cs index 55a3d5601f..2456d0aea4 100644 --- a/src/Avalonia.Controls/PixelPointEventArgs.cs +++ b/src/Avalonia.Controls/PixelPointEventArgs.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls /// /// Initializes a new instance of the class. /// - /// The data. + /// The data. public PixelPointEventArgs(PixelPoint point) { Point = point; From 7b8b6374a056191feb2c90d916872107d80e359a Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Thu, 14 Feb 2019 21:17:33 +0000 Subject: [PATCH 40/46] Scaling of 1 on monitors >= FullHD --- src/Avalonia.X11/X11Screens.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index f2a0520c10..38f685ed0d 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -99,6 +99,8 @@ namespace Avalonia.X11 { if (mon.MWidth == 0) density = 1; + else if (mon.Width <= 1920) + density = 1; else density = X11Screen.GuessPixelDensity(mon.Width, mon.MWidth); } @@ -237,7 +239,14 @@ namespace Avalonia.X11 } else if (pixelDensity == null) { - PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); + if (bounds.Width <= 1920) + { + PixelDensity = 1; + } + else + { + PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); + } } else { From 131a4d90eff1a65d4484a55e1a10280d4dfa90d5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Feb 2019 11:09:34 +0100 Subject: [PATCH 41/46] Remove Avalonia.ISupportInitialize. This is a relic from when we were targeting a PCL profile that didn't have `System.ComponentModel.ISupportInitialize`. Now that we have that, use it instead. --- src/Avalonia.Base/ISupportInitialize.cs | 22 ------------ src/Avalonia.Controls/Control.cs | 1 + .../Embedding/EmbeddableControlRoot.cs | 1 + .../Embedding/Offscreen/OffscreenTopLevel.cs | 1 + src/Avalonia.Controls/WindowBase.cs | 1 + .../AvaloniaXamlLoader.cs | 1 + .../PortableXaml/AvaloniaXamlObjectWriter.cs | 35 ------------------- .../Primitives/SelectingItemsControlTests.cs | 1 + .../Xaml/InitializationOrderTracker.cs | 3 +- .../StyledElementTests.cs | 1 + 10 files changed, 9 insertions(+), 58 deletions(-) delete mode 100644 src/Avalonia.Base/ISupportInitialize.cs diff --git a/src/Avalonia.Base/ISupportInitialize.cs b/src/Avalonia.Base/ISupportInitialize.cs deleted file mode 100644 index 04e3d72e6c..0000000000 --- a/src/Avalonia.Base/ISupportInitialize.cs +++ /dev/null @@ -1,22 +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. - -namespace Avalonia -{ - /// - /// Specifies that this object supports a simple, transacted notification for batch - /// initialization. - /// - public interface ISupportInitialize - { - /// - /// Signals the object that initialization is starting. - /// - void BeginInit(); - - /// - /// Signals the object that initialization is complete. - /// - void EndInit(); - } -} diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index a00d586233..a7ee027e70 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -1,6 +1,7 @@ // 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.ComponentModel; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 224af979ab..43beb923e5 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls.Platform; using Avalonia.Input; using Avalonia.Platform; diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index 8b39cc03b8..c4f83ffd54 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Styling; namespace Avalonia.Controls.Embedding.Offscreen diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 56ffd315f1..363af05a0b 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 2720e674cc..b99864b050 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -8,6 +8,7 @@ using Avalonia.Platform; using Portable.Xaml; using System; using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Reflection; using System.Runtime.Serialization; diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs index 5d1a98f6f8..9fa6c26c35 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlObjectWriter.cs @@ -77,40 +77,15 @@ namespace Avalonia.Markup.Xaml.PortableXaml _delayedValuesHelper.ApplyAll(); } - protected internal override void OnAfterBeginInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterBeginInit(value); - } - - protected internal override void OnAfterEndInit(object value) - { - //not called for avalonia objects - //as it's called inly for - //Portable.Xaml.ComponentModel.ISupportInitialize - base.OnAfterEndInit(value); - } - protected internal override void OnAfterProperties(object value) { _delayedValuesHelper.EndInit(value); base.OnAfterProperties(value); - - //AfterEndInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleEndEdit(value); } protected internal override void OnBeforeProperties(object value) { - //OnAfterBeginInit is not called as it supports only - //Portable.Xaml.ComponentModel.ISupportInitialize - //and we have Avalonia.ISupportInitialize so we need some hacks - HandleBeginInit(value); if (value != null) _delayedValuesHelper.BeginInit(value); @@ -127,16 +102,6 @@ namespace Avalonia.Markup.Xaml.PortableXaml return base.OnSetValue(target, member, value); } - private void HandleBeginInit(object value) - { - (value as Avalonia.ISupportInitialize)?.BeginInit(); - } - - private void HandleEndEdit(object value) - { - (value as Avalonia.ISupportInitialize)?.EndInit(); - } - public override void WriteStartMember(XamlMember property) { foreach(var d in DesignDirectives) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index bbe1d85acb..2df925301f 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; +using System.ComponentModel; using System.Linq; using Avalonia.Collections; using Avalonia.Controls.Presenters; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs index 3ecb2d9f37..104f46cbac 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/InitializationOrderTracker.cs @@ -4,6 +4,7 @@ using Avalonia.Controls; using Avalonia.LogicalTree; using System.Collections.Generic; +using System.ComponentModel; namespace Avalonia.Markup.Xaml.UnitTests.Xaml { @@ -39,4 +40,4 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Order.Add($"EndInit {InitState}"); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs index 4096dcf380..4970addd81 100644 --- a/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs +++ b/tests/Avalonia.Styling.UnitTests/StyledElementTests.cs @@ -10,6 +10,7 @@ using Avalonia.UnitTests; using Xunit; using Avalonia.LogicalTree; using Avalonia.Controls; +using System.ComponentModel; namespace Avalonia.Styling.UnitTests { From 27565d80bde42495edd9f1861ed3cc2fba50b941 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Feb 2019 10:24:25 +0000 Subject: [PATCH 42/46] [X11] put FullHd res check inside GuessPixelDensity. --- src/Avalonia.X11/X11Screens.cs | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index 38f685ed0d..ad5cad7eae 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -11,6 +11,7 @@ namespace Avalonia.X11 { class X11Screens : IScreenImpl { + private const int FullHDWidth = 1920; private IX11Screens _impl; public X11Screens(IX11Screens impl) @@ -99,8 +100,6 @@ namespace Avalonia.X11 { if (mon.MWidth == 0) density = 1; - else if (mon.Width <= 1920) - density = 1; else density = X11Screen.GuessPixelDensity(mon.Width, mon.MWidth); } @@ -239,14 +238,7 @@ namespace Avalonia.X11 } else if (pixelDensity == null) { - if (bounds.Width <= 1920) - { - PixelDensity = 1; - } - else - { - PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); - } + PixelDensity = GuessPixelDensity(bounds.Width, physicalSize.Value.Width); } else { @@ -256,6 +248,6 @@ namespace Avalonia.X11 } public static double GuessPixelDensity(double pixelWidth, double mmWidth) - => Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96)); + => pixelWidth <= FullHDWidth ? 1 : Math.Max(1, Math.Round(pixelWidth / mmWidth * 25.4 / 96)); } } From 1a661e657f270a33a6152e56789fa4b2296b76fe Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Fri, 15 Feb 2019 11:14:07 +0000 Subject: [PATCH 43/46] fix error. --- src/Avalonia.X11/X11Screens.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index ad5cad7eae..6bfc8779da 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -11,7 +11,6 @@ namespace Avalonia.X11 { class X11Screens : IScreenImpl { - private const int FullHDWidth = 1920; private IX11Screens _impl; public X11Screens(IX11Screens impl) @@ -219,6 +218,7 @@ namespace Avalonia.X11 class X11Screen { + private const int FullHDWidth = 1920; public bool Primary { get; } public string Name { get; set; } public PixelRect Bounds { get; set; } From a88c1473da0f05a409bb9c7160ff1a1800791702 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 18 Feb 2019 00:16:02 +0000 Subject: [PATCH 44/46] [OSX/Avalonia.Native] fix NRE in double dispose of ScreenImpl --- src/Avalonia.Native/ScreenImpl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs index c1edd6c846..0729de9b8e 100644 --- a/src/Avalonia.Native/ScreenImpl.cs +++ b/src/Avalonia.Native/ScreenImpl.cs @@ -41,7 +41,7 @@ namespace Avalonia.Native public void Dispose () { - _native.Dispose(); + _native?.Dispose(); _native = null; } } From 71cee6ae5bb3442da56fe0c0cc3dfc19f5a1a589 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 19 Feb 2019 11:11:42 +0100 Subject: [PATCH 45/46] Fix ItemTemplateProperty being subscribed to on every ItemsControl or derived class construction. --- src/Avalonia.Controls/ItemsControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index d74078c712..3dfeae52a4 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -64,6 +64,7 @@ namespace Avalonia.Controls static ItemsControl() { ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); + ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// @@ -73,7 +74,6 @@ namespace Avalonia.Controls { PseudoClasses.Add(":empty"); SubscribeToItems(_items); - ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// From 2a4cb2c3f6399146f03469745ebd3c70136b6546 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Tue, 19 Feb 2019 11:45:40 +0100 Subject: [PATCH 46/46] Fix Style subscriptions not being removed at all. --- src/Avalonia.Styling/Styling/Style.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Style.cs b/src/Avalonia.Styling/Styling/Style.cs index 27fad58346..d799df7ac9 100644 --- a/src/Avalonia.Styling/Styling/Style.cs +++ b/src/Avalonia.Styling/Styling/Style.cs @@ -143,6 +143,7 @@ namespace Avalonia.Styling } controlSubscriptions.Add(subs); + controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); } @@ -159,8 +160,9 @@ namespace Avalonia.Styling var sub = setter.Apply(this, control, null); subs.Add(sub); } - + controlSubscriptions.Add(subs); + controlSubscriptions.Add(Disposable.Create(() => Subscriptions.Remove(subs))); Subscriptions.Add(subs); return true; } @@ -223,7 +225,7 @@ namespace Avalonia.Styling { if (!_applied.TryGetValue(control, out var subscriptions)) { - subscriptions = new CompositeDisposable(2); + subscriptions = new CompositeDisposable(3); subscriptions.Add(control.StyleDetach.Subscribe(ControlDetach)); _applied.Add(control, subscriptions); }