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("PreviewWith", typeof (Design)); + .RegisterAttached("PreviewWith", typeof (Design)); - public static void SetPreviewWith(Style target, Control control) + public static void SetPreviewWith(AvaloniaObject target, Control control) { target.SetValue(PreviewWithProperty, control); } - public static Control GetPreviewWith(Style target) + public static Control GetPreviewWith(AvaloniaObject target) { return target.GetValue(PreviewWithProperty); } 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/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index d74078c712..3dfeae52a4 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -64,6 +64,7 @@ namespace Avalonia.Controls static ItemsControl() { ItemsProperty.Changed.AddClassHandler(x => x.ItemsChanged); + ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// @@ -73,7 +74,6 @@ namespace Avalonia.Controls { PseudoClasses.Add(":empty"); SubscribeToItems(_items); - ItemTemplateProperty.Changed.AddClassHandler(x => x.ItemTemplateChanged); } /// diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index 00fca385a0..b0fb3f2b3b 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -1,56 +1,26 @@ // Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; -using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.LogicalTree; namespace Avalonia.Controls { /// /// A top-level menu control. /// - public class Menu : SelectingItemsControl, IFocusScope, IMainMenu, IMenu + public class Menu : MenuBase, IMainMenu { - /// - /// Defines the property. - /// - public static readonly DirectProperty IsOpenProperty = - AvaloniaProperty.RegisterDirect( - nameof(IsOpen), - o => o.IsOpen); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); - - /// - /// Defines the event. - /// - public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); - private static readonly ITemplate DefaultPanel = new FuncTemplate(() => new StackPanel { Orientation = Orientation.Horizontal }); - private readonly IMenuInteractionHandler _interaction; - private bool _isOpen; /// /// Initializes a new instance of the class. /// public Menu() { - _interaction = AvaloniaLocator.Current.GetService() ?? - new DefaultMenuInteractionHandler(); } /// @@ -58,82 +28,17 @@ namespace Avalonia.Controls /// /// The menu interaction handler. public Menu(IMenuInteractionHandler interactionHandler) + : base(interactionHandler) { - Contract.Requires(interactionHandler != null); - - _interaction = interactionHandler; } - /// - /// Initializes static members of the class. - /// static Menu() { ItemsPanelProperty.OverrideDefaultValue(typeof(Menu), DefaultPanel); - MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); - } - - /// - /// Gets a value indicating whether the menu is open. - /// - public bool IsOpen - { - get { return _isOpen; } - private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } - } - - /// - 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 a is opened. - /// - public event EventHandler MenuOpened - { - add { AddHandler(MenuOpenedEvent, value); } - remove { RemoveHandler(MenuOpenedEvent, value); } - } - - /// - /// Occurs when a is closed. - /// - public event EventHandler MenuClosed - { - add { AddHandler(MenuClosedEvent, value); } - remove { RemoveHandler(MenuClosedEvent, value); } - } - - /// - /// Closes the menu. - /// - public void Close() + public override void Close() { if (IsOpen) { @@ -153,10 +58,8 @@ namespace Avalonia.Controls } } - /// - /// Opens the menu in response to the Alt/F10 key. - /// - public void Open() + /// + public override void Open() { if (!IsOpen) { @@ -170,15 +73,6 @@ namespace Avalonia.Controls } } - /// - bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); - - /// - protected override IItemContainerGenerator CreateItemContainerGenerator() - { - return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); - } - /// protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { @@ -190,41 +84,6 @@ namespace Avalonia.Controls { inputRoot.AccessKeyHandler.MainMenu = this; } - - _interaction.Attach(this); - } - - /// - protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) - { - base.OnDetachedFromVisualTree(e); - _interaction.Detach(this); - } - - /// - protected override void OnKeyDown(KeyEventArgs e) - { - // Don't handle here: let the interaction handler handle it. - } - - /// - /// Called when a submenu opens somewhere in the menu. - /// - /// The event args. - protected virtual void OnSubmenuOpened(RoutedEventArgs e) - { - if (e.Source is MenuItem menuItem && menuItem.Parent == this) - { - foreach (var child in this.GetLogicalChildren().OfType()) - { - if (child != menuItem && child.IsSubMenuOpen) - { - child.IsSubMenuOpen = false; - } - } - } - - IsOpen = true; } } } diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs new file mode 100644 index 0000000000..d6eb40360b --- /dev/null +++ b/src/Avalonia.Controls/MenuBase.cs @@ -0,0 +1,192 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls.Generators; +using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls +{ + /// + /// Base class for menu controls. + /// + public abstract class MenuBase : SelectingItemsControl, IFocusScope, IMenu + { + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsOpen), + o => o.IsOpen); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuOpenedEvent = + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + + /// + /// Defines the event. + /// + public static readonly RoutedEvent MenuClosedEvent = + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + + private bool _isOpen; + + /// + /// Initializes a new instance of the class. + /// + public MenuBase() + { + InteractionHandler = new DefaultMenuInteractionHandler(false); + } + + /// + /// Initializes a new instance of the class. + /// + /// The menu interaction handler. + public MenuBase(IMenuInteractionHandler interactionHandler) + { + Contract.Requires(interactionHandler != null); + + InteractionHandler = interactionHandler; + } + + /// + /// Initializes static members of the class. + /// + static MenuBase() + { + MenuItem.SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); + } + + /// + /// Gets a value indicating whether the menu is open. + /// + public bool IsOpen + { + get { return _isOpen; } + protected set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } + } + + /// + IMenuInteractionHandler IMenu.InteractionHandler => InteractionHandler; + + /// + 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(); + } + } + + /// + /// Gets the interaction handler for the menu. + /// + protected IMenuInteractionHandler InteractionHandler { get; } + + /// + /// Occurs when a is opened. + /// + public event EventHandler MenuOpened + { + add { AddHandler(MenuOpenedEvent, value); } + remove { RemoveHandler(MenuOpenedEvent, value); } + } + + /// + /// Occurs when a is closed. + /// + public event EventHandler MenuClosed + { + add { AddHandler(MenuClosedEvent, value); } + remove { RemoveHandler(MenuClosedEvent, value); } + } + + /// + /// Closes the menu. + /// + public abstract void Close(); + + /// + /// Opens the menu. + /// + public abstract void Open(); + + /// + bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap); + + /// + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new ItemContainerGenerator(this, MenuItem.HeaderProperty, null); + } + + /// + protected override void OnKeyDown(KeyEventArgs e) + { + // Don't handle here: let the interaction handler handle it. + } + + /// + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + InteractionHandler.Attach(this); + } + + /// + protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnDetachedFromVisualTree(e); + InteractionHandler.Detach(this); + } + + /// + /// Called when a submenu opens somewhere in the menu. + /// + /// The event args. + protected virtual void OnSubmenuOpened(RoutedEventArgs e) + { + if (e.Source is MenuItem menuItem && menuItem.Parent == this) + { + foreach (var child in this.GetLogicalChildren().OfType()) + { + if (child != menuItem && child.IsSubMenuOpen) + { + child.IsSubMenuOpen = false; + } + } + } + + IsOpen = true; + } + } +} 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/NumericUpDown/NumericUpDown.cs b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs index 18cef7d64e..f8ae5c9690 100644 --- a/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs +++ b/src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs @@ -965,11 +965,11 @@ namespace Avalonia.Controls { if (value < Minimum) { - throw new ArgumentOutOfRangeException(nameof(Minimum), string.Format("Value must be greater than Minimum value of {0}", Minimum)); + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be greater than Minimum value of {0}", Minimum)); } else if (value > Maximum) { - throw new ArgumentOutOfRangeException(nameof(Maximum), string.Format("Value must be less than Maximum value of {0}", Maximum)); + throw new ArgumentOutOfRangeException(nameof(value), string.Format("Value must be less than Maximum value of {0}", Maximum)); } } diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs index 6b03e67897..942104d61b 100644 --- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs +++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs @@ -13,18 +13,21 @@ namespace Avalonia.Controls.Platform /// public class DefaultMenuInteractionHandler : IMenuInteractionHandler { + private readonly bool _isContextMenu; private IDisposable _inputManagerSubscription; private IRenderRoot _root; - public DefaultMenuInteractionHandler() - : this(Input.InputManager.Instance, DefaultDelayRun) + public DefaultMenuInteractionHandler(bool isContextMenu) + : this(isContextMenu, Input.InputManager.Instance, DefaultDelayRun) { } public DefaultMenuInteractionHandler( + bool isContextMenu, IInputManager inputManager, Action delayRun) { + _isContextMenu = isContextMenu; InputManager = inputManager; DelayRun = delayRun; } @@ -59,7 +62,7 @@ namespace Avalonia.Controls.Platform window.Deactivated += WindowDeactivated; } - _inputManagerSubscription = InputManager.Process.Subscribe(RawInput); + _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput); } public virtual void Detach(IMenu menu) @@ -125,23 +128,16 @@ namespace Avalonia.Controls.Platform protected internal virtual void KeyDown(object sender, KeyEventArgs e) { - var item = GetMenuItem(e.Source as IControl); - - if (item != null) - { - KeyDown(item, e); - } + KeyDown(GetMenuItem(e.Source as IControl), e); } protected internal virtual void KeyDown(IMenuItem item, KeyEventArgs e) { - Contract.Requires(item != null); - switch (e.Key) { case Key.Up: case Key.Down: - if (item.IsTopLevel) + if (item?.IsTopLevel == true) { if (item.HasSubMenu && !item.IsSubMenuOpen) { @@ -156,7 +152,7 @@ namespace Avalonia.Controls.Platform break; case Key.Left: - if (item.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) + if (item?.Parent is IMenuItem parent && !parent.IsTopLevel && parent.IsSubMenuOpen) { parent.Close(); parent.Focus(); @@ -169,7 +165,7 @@ namespace Avalonia.Controls.Platform break; case Key.Right: - if (!item.IsTopLevel && item.HasSubMenu) + if (item != null && !item.IsTopLevel && item.HasSubMenu) { Open(item, true); e.Handled = true; @@ -181,47 +177,65 @@ namespace Avalonia.Controls.Platform break; case Key.Enter: - if (!item.HasSubMenu) + if (item != null) { - Click(item); - } - else - { - Open(item, true); - } + if (!item.HasSubMenu) + { + Click(item); + } + else + { + Open(item, true); + } - e.Handled = true; + e.Handled = true; + } break; case Key.Escape: - if (item.Parent != null) + if (item?.Parent != null) { item.Parent.Close(); item.Parent.Focus(); - e.Handled = true; } + else + { + Menu.Close(); + } + + e.Handled = true; break; default: var direction = e.Key.ToNavigationDirection(); - if (direction.HasValue && item.Parent?.MoveSelection(direction.Value, true) == true) + if (direction.HasValue) { - // If the the parent is an IMenu which successfully moved its selection, - // and the current menu is open then close the current menu and open the - // new menu. - if (item.IsSubMenuOpen && item.Parent is IMenu) + if (item == null && _isContextMenu) { - item.Close(); - Open(item.Parent.SelectedItem, true); + if (Menu.MoveSelection(direction.Value, true) == true) + { + e.Handled = true; + } + } + else if (item.Parent?.MoveSelection(direction.Value, true) == true) + { + // If the the parent is an IMenu which successfully moved its selection, + // and the current menu is open then close the current menu and open the + // new menu. + if (item.IsSubMenuOpen && item.Parent is IMenu) + { + item.Close(); + Open(item.Parent.SelectedItem, true); + } + e.Handled = true; } - e.Handled = true; } break; } - if (!e.Handled && item.Parent is IMenuItem parentItem) + if (!e.Handled && item?.Parent is IMenuItem parentItem) { KeyDown(parentItem, e); } 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 +} 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/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index d9070197b6..f349bcf059 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -252,9 +252,9 @@ namespace Avalonia.Controls.Primitives else { var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot != null && parentPopuproot.Parent != null) + if (parentPopuproot?.Parent is Popup popup) { - ((Popup)(parentPopuproot.Parent)).Closed += ParentClosed; + popup.Closed += ParentClosed; } } _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); @@ -293,9 +293,9 @@ namespace Avalonia.Controls.Primitives else { var parentPopuproot = _topLevel as PopupRoot; - if (parentPopuproot != null && parentPopuproot.Parent != null) + if (parentPopuproot?.Parent is Popup popup) { - ((Popup)parentPopuproot.Parent).Closed -= ParentClosed; + popup.Closed -= ParentClosed; } } _nonClientListener?.Dispose(); diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index e1767fca36..6293cbfbfd 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) @@ -265,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 { @@ -301,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) @@ -323,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(); diff --git a/src/Avalonia.Controls/Shapes/Shape.cs b/src/Avalonia.Controls/Shapes/Shape.cs index 0387328a46..57dbeba1cc 100644 --- a/src/Avalonia.Controls/Shapes/Shape.cs +++ b/src/Avalonia.Controls/Shapes/Shape.cs @@ -195,7 +195,7 @@ namespace Avalonia.Controls.Shapes if (deferCalculateTransform) { _calculateTransformOnArrange = true; - return DefiningGeometry.Bounds.Size; + return DefiningGeometry?.Bounds.Size ?? Size.Empty; } else { @@ -217,17 +217,22 @@ namespace Avalonia.Controls.Shapes private Size CalculateShapeSizeAndSetTransform(Size availableSize) { - // This should probably use GetRenderBounds(strokeThickness) but then the calculations - // will multiply the stroke thickness as well, which isn't correct. - var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); - - if (_transform != transform) + if (DefiningGeometry != null) { - _transform = transform; - _renderedGeometry = null; + // This should probably use GetRenderBounds(strokeThickness) but then the calculations + // will multiply the stroke thickness as well, which isn't correct. + var (size, transform) = CalculateSizeAndTransform(availableSize, DefiningGeometry.Bounds, Stretch); + + if (_transform != transform) + { + _transform = transform; + _renderedGeometry = null; + } + + return size; } - return size; + return Size.Empty; } internal static (Size, Matrix) CalculateSizeAndTransform(Size availableSize, Rect shapeBounds, Stretch Stretch) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index 8fee31408f..a7d4b96974 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -35,13 +35,13 @@ namespace Avalonia.DesignerSupport var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null; var loaded = loader.Load(stream, localAsm, null, baseUri); - var styles = loaded as Styles; - if (styles != null) + var style = loaded as IStyle; + if (style != null) { - var substitute = styles.OfType \ 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(); + + 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/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.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index b6f7c9ec96..343d8d41f3 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -267,6 +267,74 @@ 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(); + //after few more layout cycles layoutmanager shouldn't hold any more visual for measure/arrange + lm.ExecuteLayoutPass(); + 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 => diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs index e17279013d..87b235dce7 100644 --- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Up_Opens_MenuItem_With_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Up, Source = item }; @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Down_Opens_MenuItem_With_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Down, Source = item }; @@ -41,7 +41,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Right_Selects_Next_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Right, true) == true); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Right, Source = item }; @@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Left_Selects_Previous_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(x => x.MoveSelection(NavigationDirection.Left, true) == true); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Left, Source = item }; @@ -69,7 +69,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_No_SubMenu_Causes_Click() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Enter, Source = item }; @@ -84,7 +84,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_SubMenu_Opens_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var item = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Enter, Source = item }; @@ -99,7 +99,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Escape_Closes_Parent_Menu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu); var e = new KeyEventArgs { Key = Key.Escape, Source = item }; @@ -113,7 +113,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerEnter_Opens_Item_When_Old_Item_Is_Open() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsSubMenuOpen == true && @@ -141,7 +141,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Deselects_Item_When_Menu_Not_Open() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Doesnt_Deselect_Item_When_Menu_Open() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var item = Mock.Of(x => x.IsTopLevel == true && x.Parent == menu.Object); var e = new PointerEventArgs { RoutedEvent = MenuItem.PointerLeaveItemEvent, Source = item }; @@ -175,7 +175,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Up_Selects_Previous_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Up, Source = item }; @@ -189,7 +189,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Down_Selects_Next_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Down, Source = item }; @@ -203,7 +203,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Left_Closes_Parent_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.HasSubMenu == true && x.IsSubMenuOpen == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Left, Source = item }; @@ -218,7 +218,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Right_With_SubMenu_Items_Opens_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Right, Source = item }; @@ -233,7 +233,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Right_On_TopLevel_Child_Navigates_TopLevel_Selection() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = new Mock(); var parentItem = Mock.Of(x => x.IsSubMenuOpen == true && @@ -263,7 +263,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_No_SubMenu_Causes_Click() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -279,7 +279,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Enter_On_Item_With_SubMenu_Opens_SubMenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); var e = new KeyEventArgs { Key = Key.Enter, Source = item }; @@ -294,7 +294,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void Escape_Closes_Parent_MenuItem() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true); var item = Mock.Of(x => x.Parent == parentItem); var e = new KeyEventArgs { Key = Key.Escape, Source = item }; @@ -309,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerEnter_Selects_Item() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -325,7 +325,7 @@ namespace Avalonia.Controls.UnitTests.Platform public void PointerEnter_Opens_Submenu_After_Delay() { var timer = new TestTimer(); - var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); @@ -344,7 +344,7 @@ namespace Avalonia.Controls.UnitTests.Platform public void PointerEnter_Closes_Sibling_Submenu_After_Delay() { var timer = new TestTimer(); - var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -365,7 +365,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Deselects_Item() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -381,7 +381,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Doesnt_Deselect_Sibling() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -398,7 +398,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerLeave_Doesnt_Deselect_Item_If_Pointer_Over_Submenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true && x.IsPointerOverSubMenu == true); @@ -413,7 +413,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerReleased_On_Item_With_No_SubMenu_Causes_Click() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem); @@ -430,7 +430,7 @@ namespace Avalonia.Controls.UnitTests.Platform public void Selection_Is_Correct_When_Pointer_Temporarily_Exits_Item_To_Select_SubItem() { var timer = new TestTimer(); - var target = new DefaultMenuInteractionHandler(null, timer.RunOnce); + var target = new DefaultMenuInteractionHandler(false, null, timer.RunOnce); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); @@ -467,7 +467,7 @@ namespace Avalonia.Controls.UnitTests.Platform [Fact] public void PointerPressed_On_Item_With_SubMenu_Causes_Opens_Submenu() { - var target = new DefaultMenuInteractionHandler(); + var target = new DefaultMenuInteractionHandler(false); var menu = Mock.Of(); var parentItem = Mock.Of(x => x.IsTopLevel == true && x.HasSubMenu == true && x.Parent == menu); var item = Mock.Of(x => x.Parent == parentItem && x.HasSubMenu == true); @@ -481,6 +481,23 @@ namespace Avalonia.Controls.UnitTests.Platform } } + public class ContextMenu + { + [Fact] + public void Down_Selects_Selects_First_MenuItem_When_No_Selection() + { + var target = new DefaultMenuInteractionHandler(true); + var contextMenu = Mock.Of(x => x.MoveSelection(NavigationDirection.Down, true) == true); + var e = new KeyEventArgs { Key = Key.Down, Source = contextMenu }; + + target.Attach(contextMenu); + target.KeyDown(contextMenu, e); + + Mock.Get(contextMenu).Verify(x => x.MoveSelection(NavigationDirection.Down, true)); + Assert.True(e.Handled); + } + } + private class TestTimer { private Action _action; 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; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs new file mode 100644 index 0000000000..05224c2495 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -0,0 +1,16 @@ +using Avalonia.Controls.Shapes; +using Xunit; + +namespace Avalonia.Controls.UnitTests.Shapes +{ + public class PathTests + { + [Fact] + public void Path_With_Null_Data_Does_Not_Throw_On_Measure() + { + var target = new Path(); + + target.Measure(Size.Infinity); + } + } +} diff --git a/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs index e5a477cc32..4264559ab6 100644 --- a/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs +++ b/tests/Avalonia.DesignerSupport.Tests/RemoteProtocolTests.cs @@ -9,6 +9,7 @@ using System.Reflection; using System.Threading; using System.Threading.Tasks; using Avalonia.Remote.Protocol; +using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; using Xunit; @@ -109,6 +110,16 @@ namespace Avalonia.DesignerSupport.Tests return Guid.NewGuid().ToString(); if (t == typeof(Guid)) return Guid.NewGuid(); + if (t == typeof(Exception)) + return new Exception("Here"); + if (t == typeof(ExceptionDetails)) + return new ExceptionDetails + { + ExceptionType = "Exception", + LineNumber = 5, + LinePosition = 6, + Message = "Here", + }; throw new Exception($"Doesn't know how to fabricate a random value for {t}, path {pathInfo}"); } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index b0e8a3780e..4c288b2702 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -1,11 +1,10 @@ // 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 System.Linq; using Avalonia.Controls; -using Avalonia.UnitTests; -using System; using Xunit; -using System.Collections.Generic; namespace Avalonia.Layout.UnitTests { @@ -74,7 +73,6 @@ namespace Avalonia.Layout.UnitTests } }; - var order = new List(); Size MeasureOverride(ILayoutable control, Size size) { @@ -110,7 +108,6 @@ namespace Avalonia.Layout.UnitTests } }; - var order = new List(); Size MeasureOverride(ILayoutable control, Size size) { @@ -196,9 +193,9 @@ namespace Avalonia.Layout.UnitTests Width = 100, Height = 100, }; - + var arrangeSize = default(Size); - + root.DoArrangeOverride = (_, s) => { arrangeSize = s; @@ -207,7 +204,7 @@ namespace Avalonia.Layout.UnitTests root.LayoutManager.ExecuteInitialLayoutPass(root); Assert.Equal(new Size(100, 100), arrangeSize); - + root.Width = 120; root.LayoutManager.ExecuteLayoutPass(); @@ -238,7 +235,111 @@ namespace Avalonia.Layout.UnitTests border.Height = 100; root.LayoutManager.ExecuteLayoutPass(); - Assert.Equal(new Size(100, 100), panel.DesiredSize); + Assert.Equal(new Size(100, 100), panel.DesiredSize); + } + + [Fact] + public void LayoutManager_Should_Prevent_Infinite_Loop_On_Measure() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + root.LayoutManager.ExecuteInitialLayoutPass(root); + control.Measured = false; + + int cnt = 0; + int maxcnt = 100; + control.DoMeasureOverride = (l, s) => + { + //emulate a problem in the logic of a control that triggers + //invalidate measure during measure + //it can lead to an infinite loop in layoutmanager + if (++cnt < maxcnt) + { + control.InvalidateMeasure(); + } + + return new Size(100, 100); + }; + + control.InvalidateMeasure(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(cnt < 100); + } + + [Fact] + public void LayoutManager_Should_Prevent_Infinite_Loop_On_Arrange() + { + var control = new LayoutTestControl(); + var root = new LayoutTestRoot { Child = control }; + + root.LayoutManager.ExecuteInitialLayoutPass(root); + control.Arranged = false; + + int cnt = 0; + int maxcnt = 100; + control.DoArrangeOverride = (l, s) => + { + //emulate a problem in the logic of a control that triggers + //invalidate measure during arrange + //it can lead to infinity loop in layoutmanager + if (++cnt < maxcnt) + { + control.InvalidateArrange(); + } + + return new Size(100, 100); + }; + + control.InvalidateArrange(); + + root.LayoutManager.ExecuteLayoutPass(); + + Assert.True(cnt < 100); + } + + [Fact] + public void LayoutManager_Should_Properly_Arrange_Visuals_Even_When_There_Are_Issues_With_Previous_Arranged() + { + var nonArrageableTargets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray(); + var targets = Enumerable.Range(1, 10).Select(_ => new LayoutTestControl()).ToArray(); + + StackPanel panel; + + var root = new LayoutTestRoot + { + Child = panel = new StackPanel() + }; + + panel.Children.AddRange(nonArrageableTargets); + panel.Children.AddRange(targets); + + root.LayoutManager.ExecuteInitialLayoutPass(root); + + foreach (var c in panel.Children.OfType()) + { + c.Measured = c.Arranged = false; + c.InvalidateMeasure(); + } + + foreach (var c in nonArrageableTargets) + { + c.DoArrangeOverride = (l, s) => + { + //emulate a problem in the logic of a control that triggers + //invalidate measure during arrange + c.InvalidateMeasure(); + return new Size(100, 100); + }; + } + + root.LayoutManager.ExecuteLayoutPass(); + + //altough nonArrageableTargets has rubbish logic and can't be measured/arranged properly + //layoutmanager should process properly other visuals + Assert.All(targets, c => Assert.True(c.Arranged)); } } } diff --git a/tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs new file mode 100644 index 0000000000..f047677a8a --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/LayoutQueueTests.cs @@ -0,0 +1,196 @@ +using System.Collections.Generic; +using System.Linq; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class LayoutQueueTests + { + [Fact] + public void Should_Enqueue() + { + var target = new LayoutQueue(_ => true); + var refQueue = new Queue(); + var items = new[] { "1", "2", "3" }; + + foreach (var item in items) + { + target.Enqueue(item); + refQueue.Enqueue(item); + } + + Assert.Equal(refQueue, target); + } + + [Fact] + public void Should_Dequeue() + { + var target = new LayoutQueue(_ => true); + var refQueue = new Queue(); + var items = new[] { "1", "2", "3" }; + + foreach (var item in items) + { + target.Enqueue(item); + refQueue.Enqueue(item); + } + + while (refQueue.Count > 0) + { + Assert.Equal(refQueue.Dequeue(), target.Dequeue()); + } + } + + [Fact] + public void Should_Enqueue_UniqueElements() + { + var target = new LayoutQueue(_ => true); + + var items = new[] { "1", "2", "3", "1" }; + + foreach (var item in items) + { + target.Enqueue(item); + } + + Assert.Equal(3, target.Count); + Assert.Equal(items.Take(3), target); + } + + [Fact] + public void Shouldnt_Enqueue_More_Than_Limit_In_Loop() + { + var target = new LayoutQueue(_ => true); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + } + + [Fact] + public void Shouldnt_Count_Unique_Enqueue_For_Limit_In_Loop() + { + var target = new LayoutQueue(_ => true); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + } + + [Fact] + public void Should_Enqueue_When_Condition_True_After_Loop_When_Limit_Met() + { + var target = new LayoutQueue(_ => true); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added to queue + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + + target.EndLoop(); + + //after loop should be added once + Assert.Equal(1, target.Count); + Assert.Equal("Foo", target.First()); + } + + [Fact] + public void Shouldnt_Enqueue_When_Condition_False_After_Loop_When_Limit_Met() + { + var target = new LayoutQueue(_ => false); + + //1 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.BeginLoop(3); + + target.Dequeue(); + + //2 + target.Enqueue("Foo"); + target.Dequeue(); + + //3 + target.Enqueue("Foo"); + + Assert.Equal(1, target.Count); + + target.Dequeue(); + + //4 more than limit shouldn't be added + target.Enqueue("Foo"); + + Assert.Equal(0, target.Count); + + target.EndLoop(); + + Assert.Equal(0, target.Count); + } + } +} diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs index dba4573473..1b1a96a7e2 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs @@ -1,8 +1,6 @@ using System; using Avalonia.Controls; -using Avalonia.Controls.Primitives; using Avalonia.Markup.Parsers; -using Avalonia.Styling; using Xunit; namespace Avalonia.Markup.UnitTests.Parsers @@ -15,5 +13,19 @@ namespace Avalonia.Markup.UnitTests.Parsers var target = new SelectorParser((ns, type) => typeof(TextBlock)); var result = target.Parse("TextBlock[IsPointerOver=True]"); } + + [Fact] + public void Throws_If_OfType_Type_Not_Found() + { + var target = new SelectorParser((ns, type) => null); + Assert.Throws(() => target.Parse("NotFound")); + } + + [Fact] + public void Throws_If_Is_Type_Not_Found() + { + var target = new SelectorParser((ns, type) => null); + Assert.Throws(() => target.Parse(":is(NotFound)")); + } } } 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 = @"