diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml
index 624c1a7b28..3af90f1844 100644
--- a/samples/RenderDemo/SideBar.xaml
+++ b/samples/RenderDemo/SideBar.xaml
@@ -1,5 +1,5 @@
+ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
-
+
\ 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/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/ISupportInitialize.cs b/src/Avalonia.Base/ISupportInitialize.cs
deleted file mode 100644
index 04e3d72e6c..0000000000
--- a/src/Avalonia.Base/ISupportInitialize.cs
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-namespace Avalonia
-{
- ///
- /// Specifies that this object supports a simple, transacted notification for batch
- /// initialization.
- ///
- public interface ISupportInitialize
- {
- ///
- /// Signals the object that initialization is starting.
- ///
- void BeginInit();
-
- ///
- /// Signals the object that initialization is complete.
- ///
- void EndInit();
- }
-}
diff --git a/src/Avalonia.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/Button.cs b/src/Avalonia.Controls/Button.cs
index d485924885..f572c67284 100644
--- a/src/Avalonia.Controls/Button.cs
+++ b/src/Avalonia.Controls/Button.cs
@@ -217,7 +217,7 @@ namespace Avalonia.Controls
var e = new RoutedEventArgs(ClickEvent);
RaiseEvent(e);
- if (Command != null)
+ if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
e.Handled = true;
diff --git a/src/Avalonia.Controls/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/Control.cs b/src/Avalonia.Controls/Control.cs
index a00d586233..a7ee027e70 100644
--- a/src/Avalonia.Controls/Control.cs
+++ b/src/Avalonia.Controls/Control.cs
@@ -1,6 +1,7 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs
index 93b33e0589..4f65a0aed4 100644
--- a/src/Avalonia.Controls/DropDown.cs
+++ b/src/Avalonia.Controls/DropDown.cs
@@ -56,6 +56,7 @@ namespace Avalonia.Controls
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
+ private IDisposable _subscriptionsOnOpen;
///
/// Initializes static members of the class.
@@ -149,16 +150,12 @@ namespace Avalonia.Controls
{
if (e.Key == Key.Down)
{
- if (++SelectedIndex >= ItemCount)
- SelectedIndex = 0;
-
+ SelectNext();
e.Handled = true;
}
else if (e.Key == Key.Up)
{
- if (--SelectedIndex < 0)
- SelectedIndex = ItemCount - 1;
-
+ SelectPrev();
e.Handled = true;
}
}
@@ -174,6 +171,32 @@ namespace Avalonia.Controls
}
}
+ ///
+ protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
+ {
+ base.OnPointerWheelChanged(e);
+
+ if (!e.Handled)
+ {
+ if (!IsDropDownOpen)
+ {
+ if (IsFocused)
+ {
+ if (e.Delta.Y < 0)
+ SelectNext();
+ else
+ SelectPrev();
+
+ e.Handled = true;
+ }
+ }
+ else
+ {
+ e.Handled = true;
+ }
+ }
+ }
+
///
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
@@ -223,6 +246,9 @@ namespace Avalonia.Controls
private void PopupClosed(object sender, EventArgs e)
{
+ _subscriptionsOnOpen?.Dispose();
+ _subscriptionsOnOpen = null;
+
if (CanFocus(this))
{
Focus();
@@ -232,6 +258,22 @@ namespace Avalonia.Controls
private void PopupOpened(object sender, EventArgs e)
{
TryFocusSelectedItem();
+
+ _subscriptionsOnOpen?.Dispose();
+ _subscriptionsOnOpen = null;
+
+ var toplevel = this.GetVisualRoot() as TopLevel;
+ if (toplevel != null)
+ {
+ _subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
+ {
+ //eat wheel scroll event outside dropdown popup while it's open
+ if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)
+ {
+ ev.Handled = true;
+ }
+ }, Interactivity.RoutingStrategies.Tunnel);
+ }
}
private void SelectedItemChanged(AvaloniaPropertyChangedEventArgs e)
@@ -247,7 +289,7 @@ namespace Avalonia.Controls
{
var container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
- if(container == null && SelectedItems.Count > 0)
+ if (container == null && SelectedItems.Count > 0)
{
ScrollIntoView(SelectedItems[0]);
container = ItemContainerGenerator.ContainerFromIndex(selectedIndex);
@@ -307,5 +349,25 @@ namespace Avalonia.Controls
}
}
}
+
+ private void SelectNext()
+ {
+ int next = SelectedIndex + 1;
+
+ if (next >= ItemCount)
+ next = 0;
+
+ SelectedIndex = next;
+ }
+
+ private void SelectPrev()
+ {
+ int prev = SelectedIndex - 1;
+
+ if (prev < 0)
+ prev = ItemCount - 1;
+
+ SelectedIndex = prev;
+ }
}
}
diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
index 224af979ab..43beb923e5 100644
--- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
+++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using Avalonia.Controls.Platform;
using Avalonia.Input;
using Avalonia.Platform;
diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
index 8b39cc03b8..c4f83ffd54 100644
--- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
+++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using Avalonia.Styling;
namespace Avalonia.Controls.Embedding.Offscreen
diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs
index 72379e7b53..c696fe7975 100644
--- a/src/Avalonia.Controls/Image.cs
+++ b/src/Avalonia.Controls/Image.cs
@@ -99,5 +99,22 @@ namespace Avalonia.Controls
return new Size();
}
}
+
+ ///
+ protected override Size ArrangeOverride(Size finalSize)
+ {
+ var source = Source;
+
+ if (source != null)
+ {
+ var sourceSize = new Size(source.PixelSize.Width, source.PixelSize.Height);
+ var result = Stretch.CalculateSize(finalSize, sourceSize);
+ return result;
+ }
+ else
+ {
+ return new Size();
+ }
+ }
}
}
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/MenuItem.cs b/src/Avalonia.Controls/MenuItem.cs
index 99e00ce72e..c2227b9dfc 100644
--- a/src/Avalonia.Controls/MenuItem.cs
+++ b/src/Avalonia.Controls/MenuItem.cs
@@ -287,7 +287,7 @@ namespace Avalonia.Controls
/// The click event args.
protected virtual void OnClick(RoutedEventArgs e)
{
- if (Command != null)
+ if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
e.Handled = true;
diff --git a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs
index c4da00f5d0..25b29e37e6 100644
--- a/src/Avalonia.Controls/Mixins/ContentControlMixin.cs
+++ b/src/Avalonia.Controls/Mixins/ContentControlMixin.cs
@@ -19,8 +19,8 @@ namespace Avalonia.Controls.Mixins
///
/// The adds behavior to a control which acts as a content
/// control such as and . It
- /// updates keeps the control's logical children in sync with the content being displayed by
- /// the control.
+ /// keeps the control's logical children in sync with the content being displayed by the
+ /// control.
///
public class ContentControlMixin
{
@@ -49,25 +49,42 @@ namespace Avalonia.Controls.Mixins
Contract.Requires(content != null);
Contract.Requires(logicalChildrenSelector != null);
+ void ChildChanging(object s, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (s is IControl sender && sender?.TemplatedParent is TControl parent)
+ {
+ UpdateLogicalChild(
+ sender,
+ logicalChildrenSelector(parent),
+ e.OldValue,
+ null);
+ }
+ }
+
void TemplateApplied(object s, RoutedEventArgs ev)
{
if (s is TControl sender)
{
var e = (TemplateAppliedEventArgs)ev;
- var presenter = (IControl)e.NameScope.Find(presenterName);
+ var presenter = e.NameScope.Find(presenterName) as IContentPresenter;
if (presenter != null)
{
presenter.ApplyTemplate();
var logicalChildren = logicalChildrenSelector(sender);
- var subscription = presenter
+ var subscription = new CompositeDisposable();
+
+ presenter.ChildChanging += ChildChanging;
+ subscription.Add(Disposable.Create(() => presenter.ChildChanging -= ChildChanging));
+
+ subscription.Add(presenter
.GetPropertyChangedObservable(ContentPresenter.ChildProperty)
.Subscribe(c => UpdateLogicalChild(
sender,
logicalChildren,
- c.OldValue,
- c.NewValue));
+ null,
+ c.NewValue)));
UpdateLogicalChild(
sender,
diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
index 83d8616e90..49f268c128 100644
--- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs
@@ -5,6 +5,7 @@ using System;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
+using Avalonia.Data;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Media;
@@ -82,6 +83,7 @@ namespace Avalonia.Controls.Presenters
private IControl _child;
private bool _createdChild;
+ EventHandler _childChanging;
private IDataTemplate _dataTemplate;
private readonly BorderRenderHelper _borderRenderer = new BorderRenderHelper();
@@ -188,6 +190,13 @@ namespace Avalonia.Controls.Presenters
set { SetValue(PaddingProperty, value); }
}
+ ///
+ event EventHandler IContentPresenter.ChildChanging
+ {
+ add => _childChanging += value;
+ remove => _childChanging -= value;
+ }
+
///
public sealed override void ApplyTemplate()
{
@@ -215,9 +224,30 @@ namespace Avalonia.Controls.Presenters
var newChild = CreateChild();
// Remove the old child if we're not recycling it.
- if (oldChild != null && newChild != oldChild)
+ if (newChild != oldChild)
{
- VisualChildren.Remove(oldChild);
+ if (oldChild != null)
+ {
+ VisualChildren.Remove(oldChild);
+ }
+
+ if (oldChild?.Parent == this)
+ {
+ // If we're the child's parent then the presenter isn't in a ContentControl's
+ // template.
+ LogicalChildren.Remove(oldChild);
+ }
+ else
+ {
+ // If we're in a ContentControl's template then invoke ChildChanging to let
+ // ContentControlMixin handle removing the logical child.
+ _childChanging?.Invoke(this, new AvaloniaPropertyChangedEventArgs(
+ this,
+ ChildProperty,
+ oldChild,
+ newChild,
+ BindingPriority.LocalValue));
+ }
}
// Set the DataContext if the data isn't a control.
@@ -241,11 +271,9 @@ namespace Avalonia.Controls.Presenters
Child = newChild;
- if (oldChild?.Parent == this)
- {
- LogicalChildren.Remove(oldChild);
- }
-
+ // If we're in a ContentControl's template then the child's parent will have been
+ // set by ContentControlMixin in response to Child changing. If not, then we're
+ // standalone and should make the control our own logical child.
if (newChild.Parent == null && TemplatedParent == null)
{
LogicalChildren.Add(newChild);
diff --git a/src/Avalonia.Controls/Presenters/IContentPresenter.cs b/src/Avalonia.Controls/Presenters/IContentPresenter.cs
index 3b8039f33c..78bffec93b 100644
--- a/src/Avalonia.Controls/Presenters/IContentPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/IContentPresenter.cs
@@ -1,6 +1,8 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
+using System;
+using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
namespace Avalonia.Controls.Presenters
@@ -20,5 +22,16 @@ namespace Avalonia.Controls.Presenters
/// Gets or sets the content to be displayed by the presenter.
///
object Content { get; set; }
+
+ ///
+ /// Raised when property is about to change.
+ ///
+ ///
+ /// This event should be raised after the child has been removed from the visual tree,
+ /// but before the property has changed. It is intended for consumption
+ /// by in order to update the host control's logical
+ /// children.
+ ///
+ event EventHandler ChildChanging;
}
-}
\ No newline at end of file
+}
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/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs
index 56ffd315f1..363af05a0b 100644
--- a/src/Avalonia.Controls/WindowBase.cs
+++ b/src/Avalonia.Controls/WindowBase.cs
@@ -1,4 +1,5 @@
using System;
+using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
diff --git a/src/Avalonia.Layout/LayoutManager.cs b/src/Avalonia.Layout/LayoutManager.cs
index f3540ea631..45efccc1fa 100644
--- a/src/Avalonia.Layout/LayoutManager.cs
+++ b/src/Avalonia.Layout/LayoutManager.cs
@@ -2,7 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
-using System.Collections.Generic;
using Avalonia.Logging;
using Avalonia.Threading;
@@ -13,8 +12,8 @@ namespace Avalonia.Layout
///
public class LayoutManager : ILayoutManager
{
- private readonly Queue _toMeasure = new Queue();
- private readonly Queue _toArrange = new Queue();
+ private readonly LayoutQueue _toMeasure = new LayoutQueue(v => !v.IsMeasureValid);
+ private readonly LayoutQueue _toArrange = new LayoutQueue(v => !v.IsArrangeValid);
private bool _queued;
private bool _running;
@@ -80,6 +79,9 @@ namespace Avalonia.Layout
var stopwatch = new System.Diagnostics.Stopwatch();
stopwatch.Start();
+ _toMeasure.BeginLoop(MaxPasses);
+ _toArrange.BeginLoop(MaxPasses);
+
try
{
for (var pass = 0; pass < MaxPasses; ++pass)
@@ -98,6 +100,9 @@ namespace Avalonia.Layout
_running = false;
}
+ _toMeasure.EndLoop();
+ _toArrange.EndLoop();
+
stopwatch.Stop();
Logger.Information(LogArea.Layout, this, "Layout pass finished in {Time}", stopwatch.Elapsed);
}
@@ -112,7 +117,7 @@ namespace Avalonia.Layout
Arrange(root);
// Running the initial layout pass may have caused some control to be invalidated
- // so run a full layout pass now (this usually due to scrollbars; its not known
+ // so run a full layout pass now (this usually due to scrollbars; its not known
// whether they will need to be shown until the layout pass has run and if the
// first guess was incorrect the layout will need to be updated).
ExecuteLayoutPass();
@@ -133,7 +138,7 @@ namespace Avalonia.Layout
private void ExecuteArrangePass()
{
- while (_toArrange.Count > 0 && _toMeasure.Count == 0)
+ while (_toArrange.Count > 0)
{
var control = _toArrange.Dequeue();
diff --git a/src/Avalonia.Layout/LayoutQueue.cs b/src/Avalonia.Layout/LayoutQueue.cs
new file mode 100644
index 0000000000..ce40fdde49
--- /dev/null
+++ b/src/Avalonia.Layout/LayoutQueue.cs
@@ -0,0 +1,79 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Avalonia.Layout
+{
+ internal class LayoutQueue : IReadOnlyCollection
+ {
+ private struct Info
+ {
+ public bool Active;
+ public int Count;
+ }
+
+ public LayoutQueue(Func shouldEnqueue)
+ {
+ _shouldEnqueue = shouldEnqueue;
+ }
+
+ private Func _shouldEnqueue;
+ private Queue _inner = new Queue();
+ private Dictionary _loopQueueInfo = new Dictionary();
+ private int _maxEnqueueCountPerLoop = 1;
+
+ public int Count => _inner.Count;
+
+ public IEnumerator GetEnumerator() => (_inner as IEnumerable).GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+
+ public T Dequeue()
+ {
+ var result = _inner.Dequeue();
+
+ if (_loopQueueInfo.TryGetValue(result, out var info))
+ {
+ info.Active = false;
+ _loopQueueInfo[result] = info;
+ }
+
+ return result;
+ }
+
+ public void Enqueue(T item)
+ {
+ _loopQueueInfo.TryGetValue(item, out var info);
+
+ if (!info.Active && info.Count < _maxEnqueueCountPerLoop)
+ {
+ _inner.Enqueue(item);
+ _loopQueueInfo[item] = new Info() { Active = true, Count = info.Count + 1 };
+ }
+ }
+
+ public void BeginLoop(int maxEnqueueCountPerLoop)
+ {
+ _maxEnqueueCountPerLoop = maxEnqueueCountPerLoop;
+ }
+
+ public void EndLoop()
+ {
+ var notfinalized = _loopQueueInfo.Where(v => v.Value.Count == _maxEnqueueCountPerLoop).ToArray();
+
+ _loopQueueInfo.Clear();
+
+ //prevent layout cycle but add to next layout the non arranged/measured items that might have caused cycle
+ //one more time as a final attempt
+ foreach (var item in notfinalized)
+ {
+ if (_shouldEnqueue(item.Key))
+ {
+ _loopQueueInfo[item.Key] = new Info() { Active = true, Count = item.Value.Count + 1 };
+ _inner.Enqueue(item.Key);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Layout/Properties/AssemblyInfo.cs b/src/Avalonia.Layout/Properties/AssemblyInfo.cs
index 70fc1e9330..392ad323e5 100644
--- a/src/Avalonia.Layout/Properties/AssemblyInfo.cs
+++ b/src/Avalonia.Layout/Properties/AssemblyInfo.cs
@@ -1,6 +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.Runtime.CompilerServices;
using Avalonia.Metadata;
+[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests")]
+
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")]
+
diff --git a/src/Avalonia.Native/ScreenImpl.cs b/src/Avalonia.Native/ScreenImpl.cs
index c1edd6c846..0729de9b8e 100644
--- a/src/Avalonia.Native/ScreenImpl.cs
+++ b/src/Avalonia.Native/ScreenImpl.cs
@@ -41,7 +41,7 @@ namespace Avalonia.Native
public void Dispose ()
{
- _native.Dispose();
+ _native?.Dispose();
_native = null;
}
}
diff --git a/src/Avalonia.ReactiveUI/RoutedViewHost.cs b/src/Avalonia.ReactiveUI/RoutedViewHost.cs
new file mode 100644
index 0000000000..726e086d9c
--- /dev/null
+++ b/src/Avalonia.ReactiveUI/RoutedViewHost.cs
@@ -0,0 +1,224 @@
+using System;
+using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Avalonia.Animation;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using ReactiveUI;
+using Splat;
+
+namespace Avalonia
+{
+ ///
+ /// This control hosts the View associated with ReactiveUI RoutingState,
+ /// and will display the View and wire up the ViewModel whenever a new
+ /// ViewModel is navigated to. Nested routing is also supported.
+ ///
+ ///
+ ///
+ /// ReactiveUI routing consists of an IScreen that contains current
+ /// RoutingState, several IRoutableViewModels, and a platform-specific
+ /// XAML control called RoutedViewHost.
+ ///
+ ///
+ /// RoutingState manages the ViewModel navigation stack and allows
+ /// ViewModels to navigate to other ViewModels. IScreen is the root of
+ /// a navigation stack; despite the name, its views don't have to occupy
+ /// the whole screen. RoutedViewHost monitors an instance of RoutingState,
+ /// responding to any changes in the navigation stack by creating and
+ /// embedding the appropriate view.
+ ///
+ ///
+ /// Place this control to a view containing your ViewModel that implements
+ /// IScreen, and bind IScreen.Router property to RoutedViewHost.Router property.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// ]]>
+ ///
+ ///
+ ///
+ /// See
+ /// ReactiveUI routing documentation website for more info.
+ ///
+ ///
+ public class RoutedViewHost : UserControl, IActivatable, IEnableLogger
+ {
+ ///
+ /// The router dependency property.
+ ///
+ public static readonly AvaloniaProperty RouterProperty =
+ AvaloniaProperty.Register(nameof(Router));
+
+ ///
+ /// The default content property.
+ ///
+ public static readonly AvaloniaProperty