diff --git a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
index 2ab6c26e68..df80931367 100644
--- a/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
+++ b/samples/BindingDemo/ViewModels/ExceptionErrorViewModel.cs
@@ -21,7 +21,7 @@ namespace BindingDemo.ViewModels
}
else
{
- throw new ArgumentOutOfRangeException("Value must be less than 10.");
+ throw new ArgumentOutOfRangeException(nameof(value), "Value must be less than 10.");
}
}
}
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
index 932f3292ff..8f147638b5 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml
@@ -10,7 +10,8 @@
HorizontalAlignment="Center"
Spacing="16">
+ Margin="16"
+ Padding="48,48,48,48">
@@ -31,7 +32,24 @@
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
index dc73bef07a..96e8b49f89 100644
--- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml.cs
@@ -1,5 +1,6 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@@ -8,6 +9,7 @@ namespace ControlCatalog.Pages
public ContextMenuPage()
{
this.InitializeComponent();
+ DataContext = new ContextMenuPageViewModel();
}
private void InitializeComponent()
diff --git a/samples/ControlCatalog/Pages/MenuPage.xaml.cs b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
index 5a07cae89f..0a77607719 100644
--- a/samples/ControlCatalog/Pages/MenuPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/MenuPage.xaml.cs
@@ -4,6 +4,7 @@ using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
using ReactiveUI;
namespace ControlCatalog.Pages
@@ -13,51 +14,7 @@ namespace ControlCatalog.Pages
public MenuPage()
{
this.InitializeComponent();
- var vm = new MenuPageViewModel();
-
- vm.MenuItems = new[]
- {
- new MenuItemViewModel
- {
- Header = "_File",
- Items = new[]
- {
- new MenuItemViewModel { Header = "_Open...", Command = vm.OpenCommand },
- new MenuItemViewModel { Header = "Save", Command = vm.SaveCommand },
- new MenuItemViewModel { Header = "-" },
- new MenuItemViewModel
- {
- Header = "Recent",
- Items = new[]
- {
- new MenuItemViewModel
- {
- Header = "File1.txt",
- Command = vm.OpenRecentCommand,
- CommandParameter = @"c:\foo\File1.txt"
- },
- new MenuItemViewModel
- {
- Header = "File2.txt",
- Command = vm.OpenRecentCommand,
- CommandParameter = @"c:\foo\File2.txt"
- },
- }
- },
- }
- },
- new MenuItemViewModel
- {
- Header = "_Edit",
- Items = new[]
- {
- new MenuItemViewModel { Header = "_Copy" },
- new MenuItemViewModel { Header = "_Paste" },
- }
- }
- };
-
- DataContext = vm;
+ DataContext = new MenuPageViewModel();
}
private void InitializeComponent()
@@ -65,51 +22,4 @@ namespace ControlCatalog.Pages
AvaloniaXamlLoader.Load(this);
}
}
-
- public class MenuPageViewModel
- {
- public MenuPageViewModel()
- {
- OpenCommand = ReactiveCommand.CreateFromTask(Open);
- SaveCommand = ReactiveCommand.Create(Save);
- OpenRecentCommand = ReactiveCommand.Create(OpenRecent);
- }
-
- public IReadOnlyList MenuItems { get; set; }
- public ReactiveCommand OpenCommand { get; }
- public ReactiveCommand SaveCommand { get; }
- public ReactiveCommand OpenRecentCommand { get; }
-
- public async Task Open()
- {
- var dialog = new OpenFileDialog();
- var result = await dialog.ShowAsync(App.Current.MainWindow);
-
- if (result != null)
- {
- foreach (var path in result)
- {
- System.Diagnostics.Debug.WriteLine($"Opened: {path}");
- }
- }
- }
-
- public void Save()
- {
- System.Diagnostics.Debug.WriteLine("Save");
- }
-
- public void OpenRecent(string path)
- {
- System.Diagnostics.Debug.WriteLine($"Open recent: {path}");
- }
- }
-
- public class MenuItemViewModel
- {
- public string Header { get; set; }
- public ICommand Command { get; set; }
- public object CommandParameter { get; set; }
- public IList Items { get; set; }
- }
}
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
diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs
index 52d1609cf9..7da7a9382b 100644
--- a/src/Avalonia.Animation/Cue.cs
+++ b/src/Avalonia.Animation/Cue.cs
@@ -30,7 +30,7 @@ namespace Avalonia.Animation
///
/// Parses a string to a object.
///
- public static object Parse(string value, CultureInfo culture)
+ public static Cue Parse(string value, CultureInfo culture)
{
string v = value;
@@ -70,7 +70,7 @@ namespace Avalonia.Animation
}
}
- public class CueTypeConverter : TypeConverter
+ public class CueTypeConverter : TypeConverter
{
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
{
diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs
index 7e8d733f1b..7601b64ce9 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));
@@ -52,32 +36,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);
}
///
@@ -98,6 +57,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 +86,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 +113,7 @@ namespace Avalonia
if (_inheritanceParent != null)
{
- _inheritanceParent.PropertyChanged += ParentPropertyChanged;
+ _inheritanceParent.InheritablePropertyChanged += ParentPropertyChanged;
}
}
}
@@ -421,6 +390,7 @@ namespace Avalonia
internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
{
+ LogIfError(property, notification);
UpdateDataValidation(property, notification);
}
@@ -452,6 +422,23 @@ namespace Avalonia
});
}
+ ///
+ /// Logs a binding error for a property.
+ ///
+ /// The property that the error occurred on.
+ /// The binding error.
+ protected internal virtual void LogBindingError(AvaloniaProperty property, Exception e)
+ {
+ Logger.Log(
+ LogEventLevel.Warning,
+ LogArea.Binding,
+ this,
+ "Error in binding to {Target}.{Property}: {Message}",
+ this,
+ property,
+ e.Message);
+ }
+
///
/// Called to update the validation state for properties for which data validation is
/// enabled.
@@ -509,6 +496,11 @@ namespace Avalonia
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
_inpcChanged(this, e2);
}
+
+ if (property.Inherits)
+ {
+ _inheritablePropertyChanged?.Invoke(this, e);
+ }
}
finally
{
@@ -628,7 +620,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);
@@ -648,7 +640,7 @@ namespace Avalonia
if (notification != null)
{
- notification.LogIfError(this, property);
+ LogIfError(property, notification);
value = notification.Value;
}
@@ -780,6 +772,29 @@ namespace Avalonia
return description?.Description ?? o.ToString();
}
+ ///
+ /// Logs a mesage if the notification represents a binding error.
+ ///
+ /// The property being bound.
+ /// The binding notification.
+ private void LogIfError(AvaloniaProperty property, BindingNotification notification)
+ {
+ if (notification.ErrorType == BindingErrorType.Error)
+ {
+ if (notification.Error is AggregateException aggregate)
+ {
+ foreach (var inner in aggregate.InnerExceptions)
+ {
+ LogBindingError(property, inner);
+ }
+ }
+ else
+ {
+ LogBindingError(property, notification.Error);
+ }
+ }
+ }
+
///
/// Logs a property set message.
///
diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs
index 5fcdf76c0f..037e0dd72e 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
{
@@ -23,6 +24,8 @@ namespace Avalonia
new Dictionary>();
private readonly Dictionary> _attachedCache =
new Dictionary>();
+ private readonly Dictionary>> _initializedCache =
+ new Dictionary>>();
///
/// Gets the instance
@@ -226,6 +229,7 @@ namespace Avalonia
}
_registeredCache.Clear();
+ _initializedCache.Clear();
}
///
@@ -261,6 +265,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);
+ }
}
}
}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
index 135935498c..e48c671a13 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/MethodAccessorPlugin.cs
@@ -21,7 +21,7 @@ namespace Avalonia.Data.Core.Plugins
{
if (method.GetParameters().Length + (method.ReturnType == typeof(void) ? 0 : 1) > 8)
{
- var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(method));
+ var exception = new ArgumentException("Cannot create a binding accessor for a method with more than 8 parameters or more than 7 parameters if it has a non-void return type.", nameof(methodName));
return new PropertyError(new BindingNotification(exception, BindingErrorType.Error));
}
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/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs
index 8cbf212381..540b1bf19b 100644
--- a/src/Avalonia.Base/IPriorityValueOwner.cs
+++ b/src/Avalonia.Base/IPriorityValueOwner.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;
using Avalonia.Data;
using Avalonia.Utilities;
@@ -28,6 +29,13 @@ namespace Avalonia
/// The notification.
void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification);
+ ///
+ /// Logs a binding error.
+ ///
+ /// The property the error occurred on.
+ /// The binding error.
+ void LogError(AvaloniaProperty property, Exception e);
+
///
/// Ensures that the current thread is the UI thread.
///
diff --git a/src/Avalonia.Base/Logging/LoggerExtensions.cs b/src/Avalonia.Base/Logging/LoggerExtensions.cs
deleted file mode 100644
index 24e44bf9de..0000000000
--- a/src/Avalonia.Base/Logging/LoggerExtensions.cs
+++ /dev/null
@@ -1,53 +0,0 @@
-using System;
-using Avalonia.Data;
-
-namespace Avalonia.Logging
-{
- internal static class LoggerExtensions
- {
- public static void LogIfError(
- this BindingNotification notification,
- object source,
- AvaloniaProperty property)
- {
- if (notification.ErrorType == BindingErrorType.Error)
- {
- if (notification.Error is AggregateException aggregate)
- {
- foreach (var inner in aggregate.InnerExceptions)
- {
- LogError(source, property, inner);
- }
- }
- else
- {
- LogError(source, property, notification.Error);
- }
- }
- }
-
- private static void LogError(object source, AvaloniaProperty property, Exception e)
- {
- var level = LogEventLevel.Warning;
-
- if (e is BindingChainException b &&
- !string.IsNullOrEmpty(b.Expression) &&
- string.IsNullOrEmpty(b.ExpressionErrorPoint))
- {
- // The error occurred at the root of the binding chain: it's possible that the
- // DataContext isn't set up yet, so log at Information level instead of Warning
- // to prevent spewing hundreds of errors.
- level = LogEventLevel.Information;
- }
-
- Logger.Log(
- level,
- LogArea.Binding,
- source,
- "Error in binding to {Target}.{Property}: {Message}",
- source,
- property,
- e.Message);
- }
- }
-}
diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs
index c8b434c6f9..89a893577f 100644
--- a/src/Avalonia.Base/PriorityValue.cs
+++ b/src/Avalonia.Base/PriorityValue.cs
@@ -197,7 +197,7 @@ namespace Avalonia
/// The binding error.
public void LevelError(PriorityLevel level, BindingNotification error)
{
- error.LogIfError(Owner, Property);
+ Owner.LogError(Property, error.Error);
}
///
diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs
index d520e2b80a..24f85ea6b1 100644
--- a/src/Avalonia.Base/ValueStore.cs
+++ b/src/Avalonia.Base/ValueStore.cs
@@ -118,6 +118,10 @@ namespace Avalonia
return dict;
}
+ public void LogError(AvaloniaProperty property, Exception e)
+ {
+ _owner.LogBindingError(property, e);
+ }
public object GetValue(AvaloniaProperty property)
{
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/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs
index a69152c42b..92293a32d6 100644
--- a/src/Avalonia.Controls/ContextMenu.cs
+++ b/src/Avalonia.Controls/ContextMenu.cs
@@ -1,34 +1,31 @@
using System;
-using System.Reactive.Linq;
-using System.Linq;
using System.ComponentModel;
+using System.Linq;
+using System.Reactive.Linq;
+using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
-using System.Collections.Generic;
+using Avalonia.Controls.Primitives;
+using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.LogicalTree;
-using Avalonia.Controls.Primitives;
namespace Avalonia.Controls
{
- public class ContextMenu : SelectingItemsControl, IMenu
+ ///
+ /// A control context menu.
+ ///
+ public class ContextMenu : MenuBase
{
- private readonly IMenuInteractionHandler _interaction;
- private bool _isOpen;
+ private static readonly ITemplate DefaultPanel =
+ new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
- ///
- /// Defines the property.
- ///
- public static readonly DirectProperty IsOpenProperty =
- AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen);
-
///
/// Initializes a new instance of the class.
///
public ContextMenu()
+ : this(new DefaultMenuInteractionHandler(true))
{
- _interaction = AvaloniaLocator.Current.GetService() ??
- new DefaultMenuInteractionHandler();
}
///
@@ -36,10 +33,8 @@ namespace Avalonia.Controls
///
/// The menu interaction handler.
public ContextMenu(IMenuInteractionHandler interactionHandler)
+ : base(interactionHandler)
{
- Contract.Requires(interactionHandler != null);
-
- _interaction = interactionHandler;
}
///
@@ -47,44 +42,10 @@ namespace Avalonia.Controls
///
static ContextMenu()
{
+ ItemsPanelProperty.OverrideDefaultValue(typeof(ContextMenu), DefaultPanel);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
}
- ///
- /// Gets a value indicating whether the popup is open
- ///
- public bool IsOpen => _isOpen;
-
- ///
- IMenuInteractionHandler IMenu.InteractionHandler => _interaction;
-
- ///
- IMenuItem IMenuElement.SelectedItem
- {
- get
- {
- var index = SelectedIndex;
- return (index != -1) ?
- (IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
- null;
- }
- set
- {
- SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
- }
- }
-
- ///
- IEnumerable IMenuElement.SubItems
- {
- get
- {
- return ItemContainerGenerator.Containers
- .Select(x => x.ContainerControl)
- .OfType();
- }
- }
-
///
/// Occurs when the value of the
///
@@ -121,7 +82,7 @@ namespace Avalonia.Controls
///
/// Opens the menu.
///
- public void Open() => Open(null);
+ public override void Open() => Open(null);
///
/// Opens a context menu on the specified control.
@@ -139,21 +100,20 @@ namespace Avalonia.Controls
ObeyScreenEdges = true
};
+ _popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
- _interaction.Attach(this);
}
((ISetLogicalParent)_popup).SetParent(control);
_popup.Child = this;
_popup.IsOpen = true;
-
- SetAndRaise(IsOpenProperty, ref _isOpen, true);
+ IsOpen = true;
}
///
/// Closes the menu.
///
- public void Close()
+ public override void Close()
{
if (_popup != null && _popup.IsVisible)
{
@@ -161,8 +121,17 @@ namespace Avalonia.Controls
}
SelectedIndex = -1;
+ IsOpen = false;
+ }
+
+ protected override IItemContainerGenerator CreateItemContainerGenerator()
+ {
+ return new MenuItemContainerGenerator(this);
+ }
- SetAndRaise(IsOpenProperty, ref _isOpen, false);
+ private void PopupOpened(object sender, EventArgs e)
+ {
+ Focus();
}
private void PopupClosed(object sender, EventArgs e)
@@ -176,7 +145,7 @@ namespace Avalonia.Controls
i.IsSubMenuOpen = false;
}
- contextMenu._isOpen = false;
+ contextMenu.IsOpen = false;
contextMenu.SelectedIndex = -1;
}
}
@@ -186,7 +155,7 @@ namespace Avalonia.Controls
var control = (Control)sender;
var contextMenu = control.ContextMenu;
- if (control.ContextMenu._isOpen)
+ if (control.ContextMenu.IsOpen)
{
if (contextMenu.CancelClosing())
return;
@@ -218,10 +187,5 @@ namespace Avalonia.Controls
ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
-
- bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap)
- {
- throw new NotImplementedException();
- }
}
}
diff --git a/src/Avalonia.Controls/Design.cs b/src/Avalonia.Controls/Design.cs
index 894240a09f..0d05e19e53 100644
--- a/src/Avalonia.Controls/Design.cs
+++ b/src/Avalonia.Controls/Design.cs
@@ -48,14 +48,14 @@ namespace Avalonia.Controls
}
public static readonly AttachedProperty PreviewWithProperty = AvaloniaProperty
- .RegisterAttached
\ No newline at end of file
+
diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs
index e7d6df5a93..fd593db991 100644
--- a/src/Avalonia.Visuals/Media/DrawingContext.cs
+++ b/src/Avalonia.Visuals/Media/DrawingContext.cs
@@ -109,6 +109,8 @@ namespace Avalonia.Media
/// The geometry.
public void DrawGeometry(IBrush brush, Pen pen, Geometry geometry)
{
+ Contract.Requires(geometry != null);
+
if (brush != null || PenIsVisible(pen))
{
PlatformImpl.DrawGeometry(brush, pen, geometry.PlatformImpl);
diff --git a/src/Avalonia.Visuals/Media/GeometryDrawing.cs b/src/Avalonia.Visuals/Media/GeometryDrawing.cs
index a26a5341c8..ac0cc1c17d 100644
--- a/src/Avalonia.Visuals/Media/GeometryDrawing.cs
+++ b/src/Avalonia.Visuals/Media/GeometryDrawing.cs
@@ -31,7 +31,10 @@
public override void Draw(DrawingContext context)
{
- context.DrawGeometry(Brush, Pen, Geometry);
+ if (Geometry != null)
+ {
+ context.DrawGeometry(Brush, Pen, Geometry);
+ }
}
public override Rect GetBounds()
@@ -41,4 +44,4 @@
return Geometry?.GetRenderBounds(pen) ?? new Rect();
}
}
-}
\ No newline at end of file
+}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
index 46d264e518..2d01b117d9 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/GeometryNode.cs
@@ -89,9 +89,14 @@ namespace Avalonia.Rendering.SceneGraph
///
public override bool HitTest(Point p)
{
- p *= Transform.Invert();
- return (Brush != null && Geometry.FillContains(p)) ||
- (Pen != null && Geometry.StrokeContains(Pen, p));
+ if (Transform.HasInverse)
+ {
+ p *= Transform.Invert();
+ return (Brush != null && Geometry.FillContains(p)) ||
+ (Pen != null && Geometry.StrokeContains(Pen, p));
+ }
+
+ return false;
}
}
}
diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
index 2fb8e84a2e..19fb54e125 100644
--- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
+++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
@@ -322,36 +322,51 @@ namespace Avalonia.Rendering.SceneGraph
}
}
+ ///
+ /// Ensures that this node draw operations have been created and are mutable (in case we are using cloned operations).
+ ///
private void EnsureDrawOperationsCreated()
{
if (_drawOperations == null)
{
_drawOperations = new List>();
- _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations));
+ _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
else if (_drawOperationsCloned)
{
_drawOperations = new List>(_drawOperations.Select(op => op.Clone()));
_drawOperationsRefCounter.Dispose();
- _drawOperationsRefCounter = RefCountable.Create(Disposable.Create(DisposeDrawOperations));
+ _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
}
- public bool Disposed { get; }
-
- public void Dispose()
+ ///
+ /// Creates disposable that will dispose all items in passed draw operations after being disposed.
+ /// It is crucial that we don't capture current instance
+ /// as draw operations can be cloned and may persist across subsequent scenes.
+ ///
+ /// Draw operations that need to be disposed.
+ /// Disposable for given draw operations.
+ private static IDisposable CreateDisposeDrawOperations(List> drawOperations)
{
- _drawOperationsRefCounter?.Dispose();
+ return Disposable.Create(() =>
+ {
+ foreach (var operation in drawOperations)
+ {
+ operation.Dispose();
+ }
+ });
}
- private void DisposeDrawOperations()
+ public bool Disposed { get; private set; }
+
+ public void Dispose()
{
- foreach (var operation in DrawOperations)
- {
- operation.Dispose();
- }
+ _drawOperationsRefCounter?.Dispose();
+
+ Disposed = true;
}
}
}
diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs
index f26c21d1b6..ab2f5307a0 100644
--- a/src/Avalonia.Visuals/Visual.cs
+++ b/src/Avalonia.Visuals/Visual.cs
@@ -8,6 +8,7 @@ using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Logging;
+using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Rendering;
using Avalonia.VisualTree;
@@ -448,6 +449,34 @@ namespace Avalonia
RaisePropertyChanged(VisualParentProperty, oldParent, newParent, BindingPriority.LocalValue);
}
+ protected override sealed void LogBindingError(AvaloniaProperty property, Exception e)
+ {
+ // Don't log a binding error unless the control is attached to a logical or visual tree.
+ // In theory this should only need to check for logical tree attachment, but in practise
+ // due to ContentControlMixin only taking effect when the template has finished being
+ // applied, some controls are attached to the visual tree before the logical tree.
+ if (((ILogical)this).IsAttachedToLogicalTree || ((IVisual)this).IsAttachedToVisualTree)
+ {
+ if (e is BindingChainException b &&
+ string.IsNullOrEmpty(b.ExpressionErrorPoint) &&
+ DataContext == null)
+ {
+ // The error occurred at the root of the binding chain and DataContext is null;
+ // don't log this - the DataContext probably hasn't been set up yet.
+ return;
+ }
+
+ Logger.Log(
+ LogEventLevel.Warning,
+ LogArea.Binding,
+ this,
+ "Error in binding to {Target}.{Property}: {Message}",
+ this,
+ property,
+ e.Message);
+ }
+ }
+
///
/// Gets the visual offset from the specified ancestor.
///
@@ -522,7 +551,7 @@ namespace Avalonia
{
if (c == null)
{
- throw new ArgumentNullException("Cannot add null to VisualChildren.");
+ throw new ArgumentNullException(nameof(c), "Cannot add null to VisualChildren.");
}
if (c.VisualParent != null)
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);
diff --git a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs
index 44d887241c..878689442d 100644
--- a/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs
+++ b/src/Gtk/Avalonia.Gtk3/ImageSurfaceFramebuffer.cs
@@ -84,7 +84,7 @@ namespace Avalonia.Gtk3
public RenderOp(GtkWidget widget, ManagedCairoSurface surface, double factor, int width, int height)
{
_widget = widget;
- _surface = surface ?? throw new ArgumentNullException();
+ _surface = surface ?? throw new ArgumentNullException(nameof(surface));
_factor = factor;
_width = width;
_height = height;
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index da40e871d5..c2043bc440 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..a1f8bf6cf6 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,29 +23,7 @@ namespace Avalonia.Markup.Xaml
///
public class AvaloniaXamlLoader
{
- private readonly AvaloniaXamlSchemaContext _context = GetContext();
-
- public bool IsDesignMode
- {
- get => _context.IsDesignMode;
- 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;
- }
+ public bool IsDesignMode { get; set; }
///
/// Initializes a new instance of the class.
@@ -188,7 +165,8 @@ namespace Avalonia.Markup.Xaml
LocalAssembly = localAssembly
};
- var reader = new XamlXmlReader(stream, _context, readerSettings);
+ var context = IsDesignMode ? AvaloniaXamlSchemaContext.DesignInstance : AvaloniaXamlSchemaContext.Instance;
+ var reader = new XamlXmlReader(stream, context, readerSettings);
object result = LoadFromReader(
reader,
diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs
new file mode 100644
index 0000000000..665e71bfea
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaEventConverter.cs
@@ -0,0 +1,99 @@
+using System;
+using System.ComponentModel;
+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.Markup.Xaml.Converters
+{
+ internal class AvaloniaEventConverter : 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);
+ }
+ }
+
+ 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/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/AvaloniaXamlSchemaContext.cs b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
index 9a493a85c0..326db884a6 100644
--- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaXamlSchemaContext.cs
@@ -1,21 +1,48 @@
-using Avalonia.Data;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using Avalonia.Data;
using Avalonia.Markup.Xaml.Context;
-using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.Styling;
using Portable.Xaml;
-using Portable.Xaml.ComponentModel;
-using System.ComponentModel;
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Reflection;
namespace Avalonia.Markup.Xaml.PortableXaml
{
internal class AvaloniaXamlSchemaContext : XamlSchemaContext
{
- public bool IsDesignMode { get; set; }
+ private static AvaloniaXamlSchemaContext s_instance;
+ private static AvaloniaXamlSchemaContext s_designInstance;
+
+ public static AvaloniaXamlSchemaContext Instance
+ {
+ get
+ {
+ if (s_instance == null)
+ {
+ s_instance = Create();
+ }
+
+ return s_instance;
+ }
+ }
+
+ public static AvaloniaXamlSchemaContext DesignInstance
+ {
+ get
+ {
+ if (s_designInstance == null)
+ {
+ s_designInstance = Create();
+ s_designInstance.IsDesignMode = true;
+ }
+
+ return s_designInstance;
+ }
+ }
+
+ public bool IsDesignMode { get; private set; }
public static AvaloniaXamlSchemaContext Create(IRuntimeTypeProvider typeProvider = null)
{
return new AvaloniaXamlSchemaContext(typeProvider ?? new AvaloniaRuntimeTypeProvider());
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
+}
diff --git a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs
index 369bd8112c..ac974e3bda 100644
--- a/src/Markup/Avalonia.Markup/Data/RelativeSource.cs
+++ b/src/Markup/Avalonia.Markup/Data/RelativeSource.cs
@@ -85,9 +85,9 @@ namespace Avalonia.Data
get { return _ancestorLevel; }
set
{
- if (_ancestorLevel <= 0)
+ if (value <= 0)
{
- throw new ArgumentOutOfRangeException("AncestorLevel may not be set to less than 1.");
+ throw new ArgumentOutOfRangeException(nameof(value), "AncestorLevel may not be set to less than 1.");
}
_ancestorLevel = value;
diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
index 8d1216e1dc..8137ac3f48 100644
--- a/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
+++ b/src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
@@ -50,10 +50,10 @@ namespace Avalonia.Markup.Parsers
{
case SelectorGrammar.OfTypeSyntax ofType:
- result = result.OfType(_typeResolver(ofType.Xmlns, ofType.TypeName));
+ result = result.OfType(Resolve(ofType.Xmlns, ofType.TypeName));
break;
case SelectorGrammar.IsSyntax @is:
- result = result.Is(_typeResolver(@is.Xmlns, @is.TypeName));
+ result = result.Is(Resolve(@is.Xmlns, @is.TypeName));
break;
case SelectorGrammar.ClassSyntax @class:
result = result.Class(@class.Class);
@@ -113,5 +113,18 @@ namespace Avalonia.Markup.Parsers
return result;
}
+
+ private Type Resolve(string xmlns, string typeName)
+ {
+ var result = _typeResolver(xmlns, typeName);
+
+ if (result == null)
+ {
+ var type = string.IsNullOrWhiteSpace(xmlns) ? typeName : xmlns + ':' + typeName;
+ throw new InvalidOperationException($"Could not resolve type '{type}'");
+ }
+
+ return result;
+ }
}
}
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