From 97db5ec6578a811837d7b28acbab66cdb7063d52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 25 Oct 2018 16:34:50 +0200 Subject: [PATCH 01/57] 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 302bf55b8a8827350795cef4945b41ffd980eb30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Jan 2019 01:15:53 +0100 Subject: [PATCH 02/57] Added failing tests for #1099. --- .../AvaloniaObjectTests_Inheritance.cs | 16 ++++++++++ .../ContentPresenterTests_InTemplate.cs | 28 +++++++++++++++- .../ContentPresenterTests_Standalone.cs | 32 ++++++++++++++++++- 3 files changed, 74 insertions(+), 2 deletions(-) diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs index ef6e03a60b..c465db50fb 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Inheritance.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.Collections.Generic; using Xunit; namespace Avalonia.Base.UnitTests @@ -115,6 +116,21 @@ namespace Avalonia.Base.UnitTests Assert.True(raised); } + [Fact] + public void PropertyChanged_Is_Raised_In_Parent_Before_Child() + { + var parent = new Class1(); + var child = new Class2 { Parent = parent }; + var result = new List(); + + parent.PropertyChanged += (s, e) => result.Add(parent); + child.PropertyChanged += (s, e) => result.Add(child); + + parent.SetValue(Class1.BazProperty, "changed"); + + Assert.Equal(new[] { parent, child }, result); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty FooProperty = diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index a524ca3e89..708e934214 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -4,6 +4,7 @@ using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.LogicalTree; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -266,6 +267,31 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.IsType(target.Child); } + + [Fact] + public void Should_Not_Bind_Old_Child_To_New_DataContext() + { + // Test for issue #1099. + var textBlock = new TextBlock + { + [!TextBlock.TextProperty] = new Binding(), + }; + + var (target, host) = CreateTarget(); + host.DataTemplates.Add(new FuncDataTemplate(x => textBlock)); + host.DataTemplates.Add(new FuncDataTemplate(x => new Canvas())); + + target.Content = "foo"; + Assert.Same(textBlock, target.Child); + + textBlock.PropertyChanged += (s, e) => + { + Assert.NotEqual(e.NewValue, "42"); + }; + + target.Content = 42; + } + (ContentPresenter presenter, ContentControl templatedParent) CreateTarget() { var templatedParent = new ContentControl @@ -288,4 +314,4 @@ namespace Avalonia.Controls.UnitTests.Presenters public IControl Child { get; set; } } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 9d65f2cba7..2facee16b7 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -14,6 +14,7 @@ using System.Linq; using Xunit; using Avalonia.Rendering; using Avalonia.Media; +using Avalonia.Data; namespace Avalonia.Controls.UnitTests.Presenters { @@ -204,7 +205,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.NotEqual(foo, logicalChildren.First()); } - [Fact] public void Changing_Background_Brush_Color_Should_Invalidate_Visual() { @@ -221,5 +221,35 @@ namespace Avalonia.Controls.UnitTests.Presenters renderer.Verify(x => x.AddDirty(target), Times.Once); } + + [Fact] + public void Should_Not_Bind_Old_Child_To_New_DataContext() + { + // Test for issue #1099. + var textBlock = new TextBlock + { + [!TextBlock.TextProperty] = new Binding(), + }; + + var target = new ContentPresenter() + { + DataTemplates = + { + new FuncDataTemplate(x => textBlock), + new FuncDataTemplate(x => new Canvas()), + }, + }; + + var root = new TestRoot(target); + target.Content = "foo"; + Assert.Same(textBlock, target.Child); + + textBlock.PropertyChanged += (s, e) => + { + Assert.NotEqual(e.NewValue, "42"); + }; + + target.Content = 42; + } } } From f75f85af2bd6d81b2deb9f571a2abd8e823441a3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 20 Jan 2019 01:19:25 +0100 Subject: [PATCH 03/57] Fix order of inherited property changed events. Previously `PropertyChanged` was raised on the child before the parent because the inherited value change was being notified by listening to the parent `PropertyChanged` event, and this event handler was added first. Added an `InheritablePropertyChanged` event which will be called only after all other property changed events have been raised and only for inheritable properties. --- src/Avalonia.Base/AvaloniaObject.cs | 37 +++++++++---------- src/Avalonia.Base/IAvaloniaObject.cs | 7 +++- .../SelectorTests_Child.cs | 1 + .../SelectorTests_Descendent.cs | 1 + .../TestControlBase.cs | 1 + .../TestTemplatedControl.cs | 1 + 6 files changed, 28 insertions(+), 20 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 7e8d733f1b..28148f6568 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -22,27 +22,11 @@ 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 EventHandler _inheritablePropertyChanged; private ValueStore _values; private ValueStore Values => _values ?? (_values = new ValueStore(this)); @@ -98,6 +82,15 @@ namespace Avalonia remove { _inpcChanged -= value; } } + /// + /// Raised when an inheritable value changes on this object. + /// + event EventHandler IAvaloniaObject.InheritablePropertyChanged + { + add { _inheritablePropertyChanged += value; } + remove { _inheritablePropertyChanged -= value; } + } + /// /// Gets or sets the parent object that inherited values /// are inherited from. @@ -118,8 +111,9 @@ namespace Avalonia { if (_inheritanceParent != null) { - _inheritanceParent.PropertyChanged -= ParentPropertyChanged; + _inheritanceParent.InheritablePropertyChanged -= ParentPropertyChanged; } + var properties = AvaloniaPropertyRegistry.Instance.GetRegistered(this) .Concat(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(this.GetType())); var inherited = (from property in properties @@ -144,7 +138,7 @@ namespace Avalonia if (_inheritanceParent != null) { - _inheritanceParent.PropertyChanged += ParentPropertyChanged; + _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged; } } } @@ -509,6 +503,11 @@ namespace Avalonia PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); _inpcChanged(this, e2); } + + if (property.Inherits) + { + _inheritablePropertyChanged?.Invoke(this, e); + } } finally { diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index c11f8ada7e..5a3829167a 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -16,6 +16,11 @@ namespace Avalonia /// event EventHandler PropertyChanged; + /// + /// Raised when an inheritable value changes on this object. + /// + event EventHandler InheritablePropertyChanged; + /// /// Gets a value. /// @@ -97,4 +102,4 @@ namespace Avalonia IObservable source, BindingPriority priority = BindingPriority.LocalValue); } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs index 0561837ad1..c6eeb1ec0e 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Child.cs @@ -89,6 +89,7 @@ namespace Avalonia.Styling.UnitTests } public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; public event EventHandler AttachedToLogicalTree; public event EventHandler DetachedFromLogicalTree; diff --git a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs index 56dad13186..aef539becd 100644 --- a/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs +++ b/tests/Avalonia.Styling.UnitTests/SelectorTests_Descendent.cs @@ -119,6 +119,7 @@ namespace Avalonia.Styling.UnitTests } public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; public event EventHandler AttachedToLogicalTree; public event EventHandler DetachedFromLogicalTree; diff --git a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs index 82be755a39..d9fabf6f5d 100644 --- a/tests/Avalonia.Styling.UnitTests/TestControlBase.cs +++ b/tests/Avalonia.Styling.UnitTests/TestControlBase.cs @@ -19,6 +19,7 @@ namespace Avalonia.Styling.UnitTests #pragma warning disable CS0067 // Event not used public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; #pragma warning restore CS0067 public string Name { get; set; } diff --git a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs index 03b2f03bf2..e92ac36e8f 100644 --- a/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs +++ b/tests/Avalonia.Styling.UnitTests/TestTemplatedControl.cs @@ -12,6 +12,7 @@ namespace Avalonia.Styling.UnitTests public abstract class TestTemplatedControl : ITemplatedControl, IStyleable { public event EventHandler PropertyChanged; + public event EventHandler InheritablePropertyChanged; public abstract Classes Classes { From f54e48d9a2027ec491c2aeab0a59465f0c525ea8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Jan 2019 18:44:32 +0100 Subject: [PATCH 04/57] Added ContentPresenter.ChildChanging. And listen for this event in `ContentControlMixin` in order to remove the logical child before setting the `DataContext`. Fixes #1099 --- .../Mixins/ContentControlMixin.cs | 29 ++++++++++--- .../Presenters/ContentPresenter.cs | 42 +++++++++++++++---- .../Presenters/IContentPresenter.cs | 15 ++++++- 3 files changed, 72 insertions(+), 14 deletions(-) diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs index c4da00f5d0..25b29e37e6 100644 --- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs +++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs @@ -19,8 +19,8 @@ namespace Avalonia.Controls.Mixins /// /// The adds behavior to a control which acts as a content /// control such as and . It - /// updates keeps the control's logical children in sync with the content being displayed by - /// the control. + /// keeps the control's logical children in sync with the content being displayed by the + /// control. /// public class ContentControlMixin { @@ -49,25 +49,42 @@ namespace Avalonia.Controls.Mixins Contract.Requires(content != null); Contract.Requires(logicalChildrenSelector != null); + void ChildChanging(object s, AvaloniaPropertyChangedEventArgs e) + { + if (s is IControl sender && sender?.TemplatedParent is TControl parent) + { + UpdateLogicalChild( + sender, + logicalChildrenSelector(parent), + e.OldValue, + null); + } + } + void TemplateApplied(object s, RoutedEventArgs ev) { if (s is TControl sender) { var e = (TemplateAppliedEventArgs)ev; - var presenter = (IControl)e.NameScope.Find(presenterName); + var presenter = e.NameScope.Find(presenterName) as IContentPresenter; if (presenter != null) { presenter.ApplyTemplate(); var logicalChildren = logicalChildrenSelector(sender); - var subscription = presenter + var subscription = new CompositeDisposable(); + + presenter.ChildChanging += ChildChanging; + subscription.Add(Disposable.Create(() => presenter.ChildChanging -= ChildChanging)); + + subscription.Add(presenter .GetPropertyChangedObservable(ContentPresenter.ChildProperty) .Subscribe(c => UpdateLogicalChild( sender, logicalChildren, - c.OldValue, - c.NewValue)); + null, + c.NewValue))); UpdateLogicalChild( sender, diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 83d8616e90..49f268c128 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -5,6 +5,7 @@ using System; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; +using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; @@ -82,6 +83,7 @@ namespace Avalonia.Controls.Presenters private IControl _child; private bool _createdChild; + EventHandler _childChanging; private IDataTemplate _dataTemplate; private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper(); @@ -188,6 +190,13 @@ namespace Avalonia.Controls.Presenters set { SetValue(PaddingProperty, value); } } + /// + event EventHandler IContentPresenter.ChildChanging + { + add => _childChanging += value; + remove => _childChanging -= value; + } + /// public sealed override void ApplyTemplate() { @@ -215,9 +224,30 @@ namespace Avalonia.Controls.Presenters var newChild = CreateChild(); // Remove the old child if we're not recycling it. - if (oldChild != null && newChild != oldChild) + if (newChild != oldChild) { - VisualChildren.Remove(oldChild); + if (oldChild != null) + { + VisualChildren.Remove(oldChild); + } + + if (oldChild?.Parent == this) + { + // If we're the child's parent then the presenter isn't in a ContentControl's + // template. + LogicalChildren.Remove(oldChild); + } + else + { + // If we're in a ContentControl's template then invoke ChildChanging to let + // ContentControlMixin handle removing the logical child. + _childChanging?.Invoke(this, new AvaloniaPropertyChangedEventArgs( + this, + ChildProperty, + oldChild, + newChild, + BindingPriority.LocalValue)); + } } // Set the DataContext if the data isn't a control. @@ -241,11 +271,9 @@ namespace Avalonia.Controls.Presenters Child = newChild; - if (oldChild?.Parent == this) - { - LogicalChildren.Remove(oldChild); - } - + // If we're in a ContentControl's template then the child's parent will have been + // set by ContentControlMixin in response to Child changing. If not, then we're + // standalone and should make the control our own logical child. if (newChild.Parent == null && TemplatedParent == null) { LogicalChildren.Add(newChild); diff --git a/src/Avalonia.Controls/Presenters/IContentPresenter.cs b/src/Avalonia.Controls/Presenters/IContentPresenter.cs index 3b8039f33c..78bffec93b 100644 --- a/src/Avalonia.Controls/Presenters/IContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/IContentPresenter.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using System; +using Avalonia.Controls.Mixins; using Avalonia.Controls.Primitives; namespace Avalonia.Controls.Presenters @@ -20,5 +22,16 @@ namespace Avalonia.Controls.Presenters /// Gets or sets the content to be displayed by the presenter. /// object Content { get; set; } + + /// + /// Raised when property is about to change. + /// + /// + /// This event should be raised after the child has been removed from the visual tree, + /// but before the property has changed. It is intended for consumption + /// by in order to update the host control's logical + /// children. + /// + event EventHandler ChildChanging; } -} \ No newline at end of file +} From fafa6952570d264ad41d181d2dde6b527519f29c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 22 Jan 2019 19:13:16 +0100 Subject: [PATCH 05/57] Use recommended ContentControl pattern. --- src/Avalonia.Controls/ContentControl.cs | 14 ++++++- .../Primitives/HeaderedContentControl.cs | 37 ++++++++++++++++++- .../Primitives/HeaderedItemsControl.cs | 21 ++++++++--- ...ol.cs => HeaderedSelectingItemsControl.cs} | 21 ++++++++--- .../HeaderedItemsControlTests .cs | 2 +- tests/Avalonia.UnitTests/TestTemplatedRoot.cs | 5 ++- 6 files changed, 85 insertions(+), 15 deletions(-) rename src/Avalonia.Controls/Primitives/{HeaderedSelectingControl.cs => HeaderedSelectingItemsControl.cs} (71%) diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 6da6da54a5..16f17ae1bd 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -97,7 +97,19 @@ namespace Avalonia.Controls /// void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - Presenter = presenter; + RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual void RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_ContentPresenter") + { + Presenter = presenter; + } } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs index 7a46e0f776..98476c9c94 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedContentControl.cs @@ -1,6 +1,8 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. +using Avalonia.Controls.Mixins; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; namespace Avalonia.Controls.Primitives @@ -20,7 +22,18 @@ namespace Avalonia.Controls.Primitives /// Defines the property. /// public static readonly StyledProperty HeaderTemplateProperty = - AvaloniaProperty.Register(nameof(HeaderTemplate)); + AvaloniaProperty.Register(nameof(HeaderTemplate)); + + /// + /// Initializes static members of the class. + /// + static HeaderedContentControl() + { + ContentControlMixin.Attach( + HeaderProperty, + x => x.LogicalChildren, + "PART_HeaderPresenter"); + } /// /// Gets or sets the header content. @@ -29,7 +42,16 @@ namespace Avalonia.Controls.Primitives { get { return GetValue(HeaderProperty); } set { SetValue(HeaderProperty, value); } - } + } + + /// + /// Gets the header presenter from the control's template. + /// + public IContentPresenter HeaderPresenter + { + get; + private set; + } /// /// Gets or sets the data template used to display the header content of the control. @@ -39,5 +61,16 @@ namespace Avalonia.Controls.Primitives get { return GetValue(HeaderTemplateProperty); } set { SetValue(HeaderTemplateProperty, value); } } + + /// + protected override void RegisterContentPresenter(IContentPresenter presenter) + { + base.RegisterContentPresenter(presenter); + + if (presenter.Name == "PART_HeaderPresenter") + { + HeaderPresenter = presenter; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs index c5aa73e56a..bda426c23b 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedItemsControl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents an with a related header. /// - public class HeaderedItemsControl : ItemsControl + public class HeaderedItemsControl : ItemsControl, IContentPresenterHost { /// /// Defines the property. @@ -40,17 +40,28 @@ namespace Avalonia.Controls.Primitives /// /// Gets the header presenter from the control's template. /// - public ContentPresenter HeaderPresenter + public IContentPresenter HeaderPresenter { get; private set; } /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); - base.OnTemplateApplied(e); + RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual void RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_HeaderPresenter") + { + HeaderPresenter = presenter; + } } } } diff --git a/src/Avalonia.Controls/Primitives/HeaderedSelectingControl.cs b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs similarity index 71% rename from src/Avalonia.Controls/Primitives/HeaderedSelectingControl.cs rename to src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs index 87bb079ae7..d59be66b2b 100644 --- a/src/Avalonia.Controls/Primitives/HeaderedSelectingControl.cs +++ b/src/Avalonia.Controls/Primitives/HeaderedSelectingItemsControl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Controls.Primitives /// /// Represents a with a related header. /// - public class HeaderedSelectingItemsControl : SelectingItemsControl + public class HeaderedSelectingItemsControl : SelectingItemsControl, IContentPresenterHost { /// /// Defines the property. @@ -40,17 +40,28 @@ namespace Avalonia.Controls.Primitives /// /// Gets the header presenter from the control's template. /// - public ContentPresenter HeaderPresenter + public IContentPresenter HeaderPresenter { get; private set; } /// - protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + void IContentPresenterHost.RegisterContentPresenter(IContentPresenter presenter) { - base.OnTemplateApplied(e); - HeaderPresenter = e.NameScope.Find("PART_HeaderPresenter"); + RegisterContentPresenter(presenter); + } + + /// + /// Called when an is registered with the control. + /// + /// The presenter. + protected virtual void RegisterContentPresenter(IContentPresenter presenter) + { + if (presenter.Name == "PART_HeaderPresenter") + { + HeaderPresenter = presenter; + } } } } diff --git a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs index 570d619963..66789ef874 100644 --- a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs +++ b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs @@ -37,7 +37,7 @@ namespace Avalonia.Controls.UnitTests target.Header = "Foo"; target.ApplyTemplate(); - target.HeaderPresenter.UpdateChild(); + ((ContentPresenter)target.HeaderPresenter).UpdateChild(); var child = target.HeaderPresenter.Child; diff --git a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs index 5d42699d3f..ef49bd2f5c 100644 --- a/tests/Avalonia.UnitTests/TestTemplatedRoot.cs +++ b/tests/Avalonia.UnitTests/TestTemplatedRoot.cs @@ -18,7 +18,10 @@ namespace Avalonia.UnitTests public TestTemplatedRoot() { - Template = new FuncControlTemplate(x => new ContentPresenter()); + Template = new FuncControlTemplate(x => new ContentPresenter + { + Name = "PART_ContentPresenter", + }); } public event EventHandler Registered From c8363ddeb715efafb41e6f8eebf31b8d61bd0433 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:05:19 +0100 Subject: [PATCH 06/57] Supply more detailed exception details. So we can display better errors in a designer. --- .../Remote/RemoteDesignerEntryPoint.cs | 12 +++++++++++- src/Avalonia.Remote.Protocol/DesignMessages.cs | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 09196e4fb7..67a93f3c9c 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -8,6 +8,7 @@ using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; using Avalonia.Threading; +using Portable.Xaml; namespace Avalonia.DesignerSupport.Remote { @@ -204,9 +205,18 @@ namespace Avalonia.DesignerSupport.Remote } catch (Exception e) { + var xamlException = e as XamlException; + s_transport.Send(new UpdateXamlResultMessage { - Error = e.ToString() + Error = e.ToString(), + Exception = new ExceptionDetails + { + ExceptionType = e.GetType().FullName, + Message = e.Message.ToString(), + LineNumber = xamlException?.LineNumber, + LinePosition = xamlException?.LinePosition, + } }); } } diff --git a/src/Avalonia.Remote.Protocol/DesignMessages.cs b/src/Avalonia.Remote.Protocol/DesignMessages.cs index f70bcef6b3..5ff16c574d 100644 --- a/src/Avalonia.Remote.Protocol/DesignMessages.cs +++ b/src/Avalonia.Remote.Protocol/DesignMessages.cs @@ -15,6 +15,7 @@ namespace Avalonia.Remote.Protocol.Designer { public string Error { get; set; } public string Handle { get; set; } + public ExceptionDetails Exception { get; set; } } [AvaloniaRemoteMessageGuid("854887CF-2694-4EB6-B499-7461B6FB96C7")] @@ -23,4 +24,11 @@ namespace Avalonia.Remote.Protocol.Designer public string SessionId { get; set; } } + public class ExceptionDetails + { + public string ExceptionType { get; set; } + public string Message { get; set; } + public int? LineNumber { get; set; } + public int? LinePosition { get; set; } + } } From 8caa0d2924aa91b4cab6b1e0673d02e4c8c863f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:06:10 +0100 Subject: [PATCH 07/57] Strong name sign Avalonia.Remote.Protocol. It needs to be signed to be used in VS. --- .../Avalonia.Remote.Protocol.csproj | 2 ++ src/Avalonia.Remote.Protocol/Key.snk | Bin 0 -> 596 bytes 2 files changed, 2 insertions(+) create mode 100644 src/Avalonia.Remote.Protocol/Key.snk diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index 871c9cd995..6684772bfe 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -2,6 +2,8 @@ netstandard2.0 AVALONIA_REMOTE_PROTOCOL;$(DefineConstants) + true + Key.snk diff --git a/src/Avalonia.Remote.Protocol/Key.snk b/src/Avalonia.Remote.Protocol/Key.snk new file mode 100644 index 0000000000000000000000000000000000000000..d6cbc36f52ccda288283effe3294eac82a5c7ef7 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097%7Y|vta8^3KR$Smx;K{fFS_SHi*Psw3 zJkx=j;*YrY^86pNz5vg5O~4U_MzBsH{`RYL9%T;+mhYMfmF$a-hma@g6C=)}fn}{@ zI6+_-6SrY={k8i>Zro$$X_F^INltv6z|DqkHoVCrb@d@)Ynl ziDFw_@9Kk?>cNGZ zkCmu~lKW60E{OV*o5~ku8U4_BVgA~yhp_&l8GDAo5|W{$&Z?`@AZ@_1&~jq@i>hZp zfr$zNo<&wCV%9Y16^ult`xUXBU{dwnHju#G_+DC2%24bQ6(nW*Ru#AksAm} zVrGJ%!vumsmQv4#B#c13373GWA)F8F&Jcl|4?#CU|8T13H{?NW1^lSD_)<`0%4AFb zT{Zy{*o=br4QtNUt*gZX42(6>YX3sMl5R$?_iRW)5tpm!JF-;s%CmTExp4_Y))=1A z5!R5C0%+Tm4TlE_P);~#r&{t`3qauN`OdjpTZIT$iHJqcKU?P+59wR{R01ve4D1B~ zFg7f2wt{GtEMYmt)ZZ)c>-o!;2RLaY_uLW_ zs%cQ6S|W66#6C;m>k<*}q>-#musb}vAeE695xEBO7zyW1c9{!H`NE?Kb$f6CtyJ66 iSF5$`(ApW(ItQ!B9xp{O{xF;%!Gd-<-CHQkxaEbh4H%~Y literal 0 HcmV?d00001 From 718c01f206c43214238f9cdf96979a733dc78f6c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:05:19 +0100 Subject: [PATCH 08/57] Supply more detailed exception details. So we can display better errors in a designer. --- .../Remote/RemoteDesignerEntryPoint.cs | 12 +++++++++++- src/Avalonia.Remote.Protocol/DesignMessages.cs | 8 ++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 09196e4fb7..67a93f3c9c 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -8,6 +8,7 @@ using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; using Avalonia.Threading; +using Portable.Xaml; namespace Avalonia.DesignerSupport.Remote { @@ -204,9 +205,18 @@ namespace Avalonia.DesignerSupport.Remote } catch (Exception e) { + var xamlException = e as XamlException; + s_transport.Send(new UpdateXamlResultMessage { - Error = e.ToString() + Error = e.ToString(), + Exception = new ExceptionDetails + { + ExceptionType = e.GetType().FullName, + Message = e.Message.ToString(), + LineNumber = xamlException?.LineNumber, + LinePosition = xamlException?.LinePosition, + } }); } } diff --git a/src/Avalonia.Remote.Protocol/DesignMessages.cs b/src/Avalonia.Remote.Protocol/DesignMessages.cs index f70bcef6b3..5ff16c574d 100644 --- a/src/Avalonia.Remote.Protocol/DesignMessages.cs +++ b/src/Avalonia.Remote.Protocol/DesignMessages.cs @@ -15,6 +15,7 @@ namespace Avalonia.Remote.Protocol.Designer { public string Error { get; set; } public string Handle { get; set; } + public ExceptionDetails Exception { get; set; } } [AvaloniaRemoteMessageGuid("854887CF-2694-4EB6-B499-7461B6FB96C7")] @@ -23,4 +24,11 @@ namespace Avalonia.Remote.Protocol.Designer public string SessionId { get; set; } } + public class ExceptionDetails + { + public string ExceptionType { get; set; } + public string Message { get; set; } + public int? LineNumber { get; set; } + public int? LinePosition { get; set; } + } } From f32832878e9729a50dbaba071440722aa93a411c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 31 Jan 2019 11:06:10 +0100 Subject: [PATCH 09/57] Strong name sign Avalonia.Remote.Protocol. It needs to be signed to be used in VS. --- .../Avalonia.Remote.Protocol.csproj | 2 ++ src/Avalonia.Remote.Protocol/Key.snk | Bin 0 -> 596 bytes 2 files changed, 2 insertions(+) create mode 100644 src/Avalonia.Remote.Protocol/Key.snk diff --git a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj index 871c9cd995..6684772bfe 100644 --- a/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj +++ b/src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj @@ -2,6 +2,8 @@ netstandard2.0 AVALONIA_REMOTE_PROTOCOL;$(DefineConstants) + true + Key.snk diff --git a/src/Avalonia.Remote.Protocol/Key.snk b/src/Avalonia.Remote.Protocol/Key.snk new file mode 100644 index 0000000000000000000000000000000000000000..d6cbc36f52ccda288283effe3294eac82a5c7ef7 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50097%7Y|vta8^3KR$Smx;K{fFS_SHi*Psw3 zJkx=j;*YrY^86pNz5vg5O~4U_MzBsH{`RYL9%T;+mhYMfmF$a-hma@g6C=)}fn}{@ zI6+_-6SrY={k8i>Zro$$X_F^INltv6z|DqkHoVCrb@d@)Ynl ziDFw_@9Kk?>cNGZ zkCmu~lKW60E{OV*o5~ku8U4_BVgA~yhp_&l8GDAo5|W{$&Z?`@AZ@_1&~jq@i>hZp zfr$zNo<&wCV%9Y16^ult`xUXBU{dwnHju#G_+DC2%24bQ6(nW*Ru#AksAm} zVrGJ%!vumsmQv4#B#c13373GWA)F8F&Jcl|4?#CU|8T13H{?NW1^lSD_)<`0%4AFb zT{Zy{*o=br4QtNUt*gZX42(6>YX3sMl5R$?_iRW)5tpm!JF-;s%CmTExp4_Y))=1A z5!R5C0%+Tm4TlE_P);~#r&{t`3qauN`OdjpTZIT$iHJqcKU?P+59wR{R01ve4D1B~ zFg7f2wt{GtEMYmt)ZZ)c>-o!;2RLaY_uLW_ zs%cQ6S|W66#6C;m>k<*}q>-#musb}vAeE695xEBO7zyW1c9{!H`NE?Kb$f6CtyJ66 iSF5$`(ApW(ItQ!B9xp{O{xF;%!Gd-<-CHQkxaEbh4H%~Y literal 0 HcmV?d00001 From ba316b89333cf0ebffcdd940f0facd3756510990 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Feb 2019 14:51:44 +0100 Subject: [PATCH 10/57] Updated Portable.Xaml. --- .../Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index ab55261737..b9f886b93a 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 ab5526173722b8988bc5ca3c03c8752ce89c0975 +Subproject commit b9f886b93ab28dd69722e72ef8cb6c33889b3749 From 521c9a38414ca4a9e80c9619f23bf1da7460f083 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 1 Feb 2019 18:17:02 +0100 Subject: [PATCH 11/57] Updated Portable.Xaml. To get better line/column info for errors. --- .../Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index b9f886b93a..452ced4782 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 b9f886b93ab28dd69722e72ef8cb6c33889b3749 +Subproject commit 452ced47823d963c1e2d0ad809d0327c9e9b5247 From 870a2f365f6409a8940616ccad5baa36dc067886 Mon Sep 17 00:00:00 2001 From: Andrey Kunchev Date: Tue, 12 Feb 2019 16:45:04 +0200 Subject: [PATCH 12/57] layoutmanager/listbox/vistualization/scroll issue unit test --- .../ListBoxTests.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index b6f7c9ec96..6939c6a081 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -267,6 +267,71 @@ namespace Avalonia.Controls.UnitTests Assert.True(true); } + [Fact] + public void LayoutManager_Should_Measure_Arrange_All() + { + var virtualizationMode = ItemVirtualizationMode.Simple; + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var items = new AvaloniaList(Enumerable.Range(1, 7).Select(v => v.ToString())); + + var wnd = new Window() { SizeToContent = SizeToContent.WidthAndHeight }; + + wnd.IsVisible = true; + + var target = new ListBox(); + + wnd.Content = target; + + var lm = wnd.LayoutManager; + + target.Height = 110; + target.Width = 50; + target.DataContext = items; + target.VirtualizationMode = virtualizationMode; + + target.ItemTemplate = new FuncDataTemplate(c => + { + var tb = new TextBlock() { Height = 10, Width = 30 }; + tb.Bind(TextBlock.TextProperty, new Data.Binding()); + return tb; + }, true); + + lm.ExecuteInitialLayoutPass(wnd); + + target.Items = items; + + lm.ExecuteLayoutPass(); + + items.Insert(3, "3+"); + lm.ExecuteLayoutPass(); + + items.Insert(4, "4+"); + lm.ExecuteLayoutPass(); + + //RESET + items.Clear(); + foreach (var i in Enumerable.Range(1, 7)) + { + items.Add(i.ToString()); + } + + //working bit better with this line no outof memory or remaining to arrange/measure ??? + //lm.ExecuteLayoutPass(); + + items.Insert(2, "2+"); + + lm.ExecuteLayoutPass(); + + var flags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic; + var toMeasure = lm.GetType().GetField("_toMeasure", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; + var toArrange = lm.GetType().GetField("_toArrange", flags).GetValue(lm) as System.Collections.Generic.IEnumerable; + + Assert.Equal(0, toMeasure.Count()); + Assert.Equal(0, toArrange.Count()); + } + } + private FuncControlTemplate ListBoxTemplate() { return new FuncControlTemplate(parent => From 03e84dea7bf86d5133a9ed79891c24899e0d771a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 13:11:53 +0100 Subject: [PATCH 13/57] Ignore missing events in the designer. Do this by registering a special type converter for EventInfo, and add in the plumbing for Portable.Xaml to use that type converter instead of its own (internal) `EventConverter`. --- .../DesignWindowLoader.cs | 5 ++ .../DesignerEventConverter.cs | 87 +++++++++++++++++++ .../AvaloniaMemberAttributeProvider.cs | 14 ++- .../PortableXaml/AvaloniaXamlType.cs | 2 +- 4 files changed, 106 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.DesignerSupport/DesignerEventConverter.cs diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 8fee31408f..2df171295f 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -12,6 +12,11 @@ namespace Avalonia.DesignerSupport { public class DesignWindowLoader { + static DesignWindowLoader() + { + AvaloniaTypeConverters.Register(typeof(EventInfo), typeof(DesignerEventConverter)); + } + public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath) { Window window; diff --git a/src/Avalonia.DesignerSupport/DesignerEventConverter.cs b/src/Avalonia.DesignerSupport/DesignerEventConverter.cs new file mode 100644 index 0000000000..b420448698 --- /dev/null +++ b/src/Avalonia.DesignerSupport/DesignerEventConverter.cs @@ -0,0 +1,87 @@ +using System; +using System.ComponentModel; +using System.Globalization; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using Portable.Xaml; + +namespace Avalonia.DesignerSupport +{ + internal class DesignerEventConverter : TypeConverter + { + public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) + { + return sourceType == typeof(string) || base.CanConvertFrom(context, sourceType); + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + var text = value as string; + if (text != null) + { + var rootObjectProvider = context.GetService(typeof(IRootObjectProvider)) as IRootObjectProvider; + var destinationTypeProvider = context.GetService(typeof(IDestinationTypeProvider)) as IDestinationTypeProvider; + if (rootObjectProvider != null && destinationTypeProvider != null) + { + var target = rootObjectProvider.RootObject; + var eventType = destinationTypeProvider.GetDestinationType(); + var eventParameters = eventType.GetRuntimeMethods().First(r => r.Name == "Invoke").GetParameters(); + // go in reverse to match System.Xaml behaviour + var methods = target.GetType().GetRuntimeMethods().Reverse(); + + // find based on exact match parameter types first + foreach (var method in methods) + { + if (method.Name != text) + continue; + var parameters = method.GetParameters(); + if (eventParameters.Length != parameters.Length) + continue; + if (parameters.Length == 0) + return method.CreateDelegate(eventType, target); + + for (int i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + var eventParam = eventParameters[i]; + if (param.ParameterType != eventParam.ParameterType) + break; + if (i == parameters.Length - 1) + return method.CreateDelegate(eventType, target); + } + } + + // EnhancedXaml: Find method with compatible base class parameters + foreach (var method in methods) + { + if (method.Name != text) + continue; + var parameters = method.GetParameters(); + if (parameters.Length == 0 || eventParameters.Length != parameters.Length) + continue; + + for (int i = 0; i < parameters.Length; i++) + { + var param = parameters[i]; + var eventParam = eventParameters[i]; + if (!param.ParameterType.GetTypeInfo().IsAssignableFrom(eventParam.ParameterType.GetTypeInfo())) + break; + if (i == parameters.Length - 1) + return method.CreateDelegate(eventType, target); + } + } + + // We want to ignore missing events in the designer, so if event handler + // wasn't found create an empty delegate. + var lambdaExpression = Expression.Lambda( + eventType, + Expression.Empty(), + eventParameters.Select(x => Expression.Parameter(x.ParameterType))); + return lambdaExpression.Compile(); + } + } + return base.ConvertFrom(context, culture, value); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs index e9f6ba6945..529cbab938 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaMemberAttributeProvider.cs @@ -49,6 +49,18 @@ namespace Avalonia.Markup.Xaml.PortableXaml //Portable.Xaml is not searching for Type Converter result = new TypeConverterAttribute(typeof(SetterValueTypeConverter)); } + else if (attributeType == typeof(TypeConverterAttribute) && _info is EventInfo) + { + // If a type converter for `EventInfo` is registered, then use that to convert + // event handler values. This is used by the designer to override the lookup + // for event handlers with a null handler. + var eventConverter = AvaloniaTypeConverters.GetTypeConverter(typeof(EventInfo)); + + if (eventConverter != null) + { + result = new TypeConverterAttribute(eventConverter); + } + } if (result == null) { @@ -68,4 +80,4 @@ namespace Avalonia.Markup.Xaml.PortableXaml private readonly MemberInfo _info; } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs index 2194223cb7..10cf716912 100644 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlType.cs @@ -385,4 +385,4 @@ namespace Avalonia.Markup.Xaml.PortableXaml { } } -} \ No newline at end of file +} From 9ed23bbf89158792812965b9f39afeab71314ef9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 16:33:27 +0100 Subject: [PATCH 14/57] Handle null modifiers collection. --- .../Remote/Server/RemoteServerTopLevelImpl.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index e1767fca36..028a78aea4 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -61,6 +61,11 @@ namespace Avalonia.Controls.Remote.Server { var result = InputModifiers.None; + if (modifiers == null) + { + return result; + } + foreach(var modifier in modifiers) { switch (modifier) From 2b34a2789213796454d62a7706dc55f3ddb37a27 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 19:36:33 +0100 Subject: [PATCH 15/57] Return a frame even for an empty control. If the user has a control with no content then we've still got a valid control so return a frame with size 0. --- .../Remote/Server/RemoteServerTopLevelImpl.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 028a78aea4..3fa0b108ec 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -270,11 +270,15 @@ namespace Avalonia.Controls.Remote.Server var bpp = fmt == ProtocolPixelFormat.Rgb565 ? 2 : 4; var data = new byte[width * height * bpp]; var handle = GCHandle.Alloc(data, GCHandleType.Pinned); + try { - _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt, - null); - Paint?.Invoke(new Rect(0, 0, width, height)); + if (width > 0 && height > 0) + { + _framebuffer = new LockedFramebuffer(handle.AddrOfPinnedObject(), new PixelSize(width, height), width * bpp, _dpi, (PixelFormat)fmt, + null); + Paint?.Invoke(new Rect(0, 0, width, height)); + } } finally { @@ -306,8 +310,7 @@ namespace Avalonia.Controls.Remote.Server return; } - if (ClientSize.Width < 1 || ClientSize.Height < 1) - return; + var format = ProtocolPixelFormat.Rgba8888; foreach(var fmt in _supportedFormats) if (fmt <= ProtocolPixelFormat.MaxValue) From 81510372b4a8d445e083b83ac988b8fb772acfbf Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 13 Feb 2019 21:01:26 +0100 Subject: [PATCH 16/57] Don't invalidate a disposed TopLevelImpl. --- .../Embedding/Offscreen/OffscreenTopLevelImpl.cs | 4 +++- .../Remote/Server/RemoteServerTopLevelImpl.cs | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs index d328e1ee88..9c53dc0c10 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevelImpl.cs @@ -11,11 +11,13 @@ namespace Avalonia.Controls.Embedding.Offscreen { private double _scaling = 1; private Size _clientSize; + public IInputRoot InputRoot { get; private set; } + public bool IsDisposed { get; private set; } public virtual void Dispose() { - //No-op + IsDisposed = true; } public IRenderer CreateRenderer(IRenderRoot root) => new ImmediateRenderer(root); diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 3fa0b108ec..6293cbfbfd 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -331,8 +331,11 @@ namespace Avalonia.Controls.Remote.Server public override void Invalidate(Rect rect) { - _invalidated = true; - Dispatcher.UIThread.Post(RenderIfNeeded); + if (!IsDisposed) + { + _invalidated = true; + Dispatcher.UIThread.Post(RenderIfNeeded); + } } public override IMouseDevice MouseDevice { get; } = new MouseDevice(); From e3e42e8ae08fb91c8f7a28a3ea858eff709639ef Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Feb 2019 22:24:45 +0100 Subject: [PATCH 17/57] Fix bad XAML in DesignWindowLoader message. --- src/Avalonia.DesignerSupport/DesignWindowLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 2df171295f..68feb2edb5 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -56,8 +56,8 @@ namespace Avalonia.DesignerSupport { new TextBlock {Text = "Styles can't be previewed without Design.PreviewWith. Add"}, new TextBlock {Text = ""}, - new TextBlock {Text = " "}, - new TextBlock {Text = ""}, + new TextBlock {Text = " "}, + new TextBlock {Text = ""}, new TextBlock {Text = "before setters in your first Style"} } }; From 8d9c23446a8d4e7cde8416c913104d41e282508a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 14 Feb 2019 23:35:36 +0100 Subject: [PATCH 18/57] Make Styles inherit from AvaloniaObject. This way we can add attached properties (such as `Design.PreviewWith`) to `Styles`. --- src/Avalonia.Styling/Styling/Styles.cs | 79 +++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 51499b737a..288cf35d08 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -2,7 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Collections; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using Avalonia.Collections; using Avalonia.Controls; @@ -12,16 +14,17 @@ namespace Avalonia.Styling /// /// A style that consists of a number of child styles. /// - public class Styles : AvaloniaList, IStyle, ISetStyleParent + public class Styles : AvaloniaObject, IAvaloniaList, IStyle, ISetStyleParent { private IResourceNode _parent; private IResourceDictionary _resources; + private AvaloniaList _styles = new AvaloniaList(); private Dictionary> _cache; public Styles() { - ResetBehavior = ResetBehavior.Remove; - this.ForEachItem( + _styles.ResetBehavior = ResetBehavior.Remove; + _styles.ForEachItem( x => { if (x.ResourceParent == null && x is ISetStyleParent setParent) @@ -57,9 +60,18 @@ namespace Avalonia.Styling () => { }); } + public event NotifyCollectionChangedEventHandler CollectionChanged + { + add => _styles.CollectionChanged += value; + remove => _styles.CollectionChanged -= value; + } + /// public event EventHandler ResourcesChanged; + /// + public int Count => _styles.Count; + /// public bool HasResources => _resources?.Count > 0 || this.Any(x => x.HasResources); @@ -94,6 +106,19 @@ namespace Avalonia.Styling /// IResourceNode IResourceNode.ResourceParent => _parent; + /// + bool ICollection.IsReadOnly => false; + + /// + IStyle IReadOnlyList.this[int index] => _styles[index]; + + /// + public IStyle this[int index] + { + get => _styles[index]; + set => _styles[index] = value; + } + /// /// Attaches the style to a control if the style's selector matches. /// @@ -172,6 +197,54 @@ namespace Avalonia.Styling return false; } + /// + public void AddRange(IEnumerable items) => _styles.AddRange(items); + + /// + public void InsertRange(int index, IEnumerable items) => _styles.InsertRange(index, items); + + /// + public void Move(int oldIndex, int newIndex) => _styles.Move(oldIndex, newIndex); + + /// + public void MoveRange(int oldIndex, int count, int newIndex) => _styles.MoveRange(oldIndex, count, newIndex); + + /// + public void RemoveAll(IEnumerable items) => _styles.RemoveAll(items); + + /// + public void RemoveRange(int index, int count) => _styles.RemoveRange(index, count); + + /// + public int IndexOf(IStyle item) => _styles.IndexOf(item); + + /// + public void Insert(int index, IStyle item) => _styles.Insert(index, item); + + /// + public void RemoveAt(int index) => _styles.RemoveAt(index); + + /// + public void Add(IStyle item) => _styles.Add(item); + + /// + public void Clear() => _styles.Clear(); + + /// + public bool Contains(IStyle item) => _styles.Contains(item); + + /// + public void CopyTo(IStyle[] array, int arrayIndex) => _styles.CopyTo(array, arrayIndex); + + /// + public bool Remove(IStyle item) => _styles.Remove(item); + + /// + public IEnumerator GetEnumerator() => _styles.GetEnumerator(); + + /// + IEnumerator IEnumerable.GetEnumerator() => _styles.GetEnumerator(); + /// void ISetStyleParent.SetParent(IResourceNode parent) { From c438f9301e8d8a53269d485cdb05f551457afef7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 15 Feb 2019 00:13:57 +0100 Subject: [PATCH 19/57] Make DesignWindowLoader expect PreviewWith on Styles. --- samples/ControlCatalog/SideBar.xaml | 8 ++++++++ src/Avalonia.Controls/Design.cs | 6 +++--- src/Avalonia.DesignerSupport/DesignWindowLoader.cs | 8 ++++---- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 625b344b8c..fea55bcb07 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -1,6 +1,14 @@ + + + + + + + + - + \ No newline at end of file From 64a0ae0921ec0625081b8a976f8df22a0e589c6a Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Mon, 25 Feb 2019 14:04:07 +0300 Subject: [PATCH 45/57] [X11] Fixed window size hints handling --- src/Avalonia.X11/X11Window.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index cbff4e38cf..3af2d5f3fe 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -557,8 +557,14 @@ namespace Avalonia.X11 private bool _systemDecorations = true; private bool _canResize = true; - private (Size minSize, Size maxSize) _scaledMinMaxSize; - private (PixelSize minSize, PixelSize maxSize) _minMaxSize; + private const int MaxWindowDimension = 100000; + + private (Size minSize, Size maxSize) _scaledMinMaxSize = + (new Size(1, 1), new Size(double.PositiveInfinity, double.PositiveInfinity)); + + private (PixelSize minSize, PixelSize maxSize) _minMaxSize = (new PixelSize(1, 1), + new PixelSize(MaxWindowDimension, MaxWindowDimension)); + private double _scaling = 1; void ScheduleInput(RawInputEventArgs args, ref XEvent xev) @@ -874,10 +880,10 @@ namespace Avalonia.X11 (int)(minSize.Width < 1 ? 1 : minSize.Width * Scaling), (int)(minSize.Height < 1 ? 1 : minSize.Height * Scaling)); - const int maxDim = 100000; + const int maxDim = MaxWindowDimension; var max = new PixelSize( - (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, minSize.Width * Scaling)), - (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, minSize.Height * Scaling))); + (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, maxSize.Width * Scaling)), + (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, maxSize.Height * Scaling))); _minMaxSize = (min, max); UpdateSizeHints(null); From f85995ed7e6e83fe1fcaef2ef3daf2dcc4287aa8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 26 Feb 2019 15:43:10 +0100 Subject: [PATCH 46/57] Move design-time handling of events... ...to `Avalonia.Markup.Xaml`. Also added tests: tests required a new instance of `AvaloniaXamlSchemaContext` for each `AvaloniaXamlLoader` as otherwise one test can affect other tests. --- .../DesignWindowLoader.cs | 5 -- .../Avalonia.Markup.Xaml.csproj | 1 + .../AvaloniaTypeConverters.cs | 4 +- .../AvaloniaXamlLoader.cs | 31 ++------- .../Converters/AvaloniaEventConverter.cs} | 30 ++++++--- .../PortableXaml/portable.xaml.github | 2 +- .../Xaml/EventTests.cs | 66 +++++++++++++++++++ 7 files changed, 99 insertions(+), 40 deletions(-) rename src/{Avalonia.DesignerSupport/DesignerEventConverter.cs => Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs} (74%) create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 5d49ddf6a1..a7d4b96974 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -12,11 +12,6 @@ namespace Avalonia.DesignerSupport { public class DesignWindowLoader { - static DesignWindowLoader() - { - AvaloniaTypeConverters.Register(typeof(EventInfo), typeof(DesignerEventConverter)); - } - public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath) { Window window; diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 38d207a31d..3c1ef84c54 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -9,6 +9,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs index c30822aacb..be92b6a8d7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaTypeConverters.cs @@ -11,6 +11,7 @@ using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml { + using System.Reflection; using Avalonia.Media; /// @@ -41,7 +42,8 @@ namespace Avalonia.Markup.Xaml { typeof(WindowIcon), typeof(IconTypeConverter) }, { typeof(CultureInfo), typeof(CultureInfoConverter) }, { typeof(Uri), typeof(AvaloniaUriTypeConverter) }, - { typeof(FontFamily), typeof(FontFamilyTypeConverter) } + { typeof(FontFamily), typeof(FontFamilyTypeConverter) }, + { typeof(EventInfo), typeof(AvaloniaEventConverter) }, }; internal static Type GetBuiltinTypeConverter(Type type) diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index b99864b050..800afb3976 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -1,21 +1,20 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using Avalonia.Controls; -using Avalonia.Markup.Data; -using Avalonia.Markup.Xaml.PortableXaml; -using Avalonia.Platform; -using Portable.Xaml; using System; using System.Collections.Generic; using System.ComponentModel; using System.IO; +using System.Linq; using System.Reflection; using System.Runtime.Serialization; -using System.Runtime.Serialization.Json; using System.Text; using System.Xml.Linq; -using System.Linq; +using Avalonia.Controls; +using Avalonia.Markup.Data; +using Avalonia.Markup.Xaml.PortableXaml; +using Avalonia.Platform; +using Portable.Xaml; namespace Avalonia.Markup.Xaml { @@ -24,7 +23,7 @@ namespace Avalonia.Markup.Xaml /// public class AvaloniaXamlLoader { - private readonly AvaloniaXamlSchemaContext _context = GetContext(); + private readonly AvaloniaXamlSchemaContext _context = AvaloniaXamlSchemaContext.Create(); public bool IsDesignMode { @@ -32,22 +31,6 @@ namespace Avalonia.Markup.Xaml set => _context.IsDesignMode = value; } - private static AvaloniaXamlSchemaContext GetContext() - { - var result = AvaloniaLocator.Current.GetService(); - - if (result == null) - { - result = AvaloniaXamlSchemaContext.Create(); - - AvaloniaLocator.CurrentMutable - .Bind() - .ToConstant(result); - } - - return result; - } - /// /// Initializes a new instance of the class. /// diff --git a/src/Avalonia.DesignerSupport/DesignerEventConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs similarity index 74% rename from src/Avalonia.DesignerSupport/DesignerEventConverter.cs rename to src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs index b420448698..665e71bfea 100644 --- a/src/Avalonia.DesignerSupport/DesignerEventConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs @@ -4,11 +4,13 @@ using System.Globalization; using System.Linq; using System.Linq.Expressions; using System.Reflection; +using Avalonia.Controls; +using Avalonia.Markup.Xaml.PortableXaml; using Portable.Xaml; -namespace Avalonia.DesignerSupport +namespace Avalonia.Markup.Xaml.Converters { - internal class DesignerEventConverter : TypeConverter + internal class AvaloniaEventConverter : TypeConverter { public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { @@ -72,13 +74,23 @@ namespace Avalonia.DesignerSupport } } - // We want to ignore missing events in the designer, so if event handler - // wasn't found create an empty delegate. - var lambdaExpression = Expression.Lambda( - eventType, - Expression.Empty(), - eventParameters.Select(x => Expression.Parameter(x.ParameterType))); - return lambdaExpression.Compile(); + var contextProvider = (IXamlSchemaContextProvider)context.GetService(typeof(IXamlSchemaContextProvider)); + var avaloniaContext = (AvaloniaXamlSchemaContext)contextProvider.SchemaContext; + + if (avaloniaContext.IsDesignMode) + { + // We want to ignore missing events in the designer, so if event handler + // wasn't found create an empty delegate. + var lambdaExpression = Expression.Lambda( + eventType, + Expression.Empty(), + eventParameters.Select(x => Expression.Parameter(x.ParameterType))); + return lambdaExpression.Compile(); + } + else + { + throw new XamlObjectWriterException($"Referenced value method {text} in type {target.GetType()} indicated by event {eventType.FullName} was not found"); + } } } return base.ConvertFrom(context, culture, value); diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index 5d2edfcc1b..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 5d2edfcc1b2988f80303b1b2f3dd2b7c3de53db7 +Subproject commit ab5526173722b8988bc5ca3c03c8752ce89c0975 diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs new file mode 100644 index 0000000000..6ea3015138 --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/EventTests.cs @@ -0,0 +1,66 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Avalonia.Controls; +using Avalonia.Input; +using Portable.Xaml; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Xaml +{ + public class EventTests + { + [Fact] + public void Event_Is_Attached() + { + var xaml = @"