Browse Source

Merge branch 'master' into refactor/value-store

pull/3255/head
Steven Kirk 6 years ago
committed by GitHub
parent
commit
b140310b37
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 12
      azure-pipelines.yml
  2. 2
      build/SharedVersion.props
  3. 4
      native/Avalonia.Native/inc/avalonia-native.h
  4. 14
      native/Avalonia.Native/src/OSX/app.mm
  5. 49
      native/Avalonia.Native/src/OSX/window.mm
  6. 4
      samples/ControlCatalog/App.xaml.cs
  7. 12
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  8. 17
      src/Avalonia.Base/Data/BindingOperations.cs
  9. 13
      src/Avalonia.Base/Data/Core/BindingExpression.cs
  10. 6
      src/Avalonia.Base/Data/Core/ExpressionObserver.cs
  11. 20
      src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs
  12. 14
      src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs
  13. 15
      src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs
  14. 25
      src/Avalonia.Base/Reactive/LightweightObservableBase.cs
  15. 2
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  16. 21
      src/Avalonia.Controls/Application.cs
  17. 5
      src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs
  18. 49
      src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs
  19. 25
      src/Avalonia.Controls/ItemsControl.cs
  20. 10
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  21. 25
      src/Avalonia.Controls/Primitives/RangeBase.cs
  22. 18
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  23. 14
      src/Avalonia.Controls/TopLevel.cs
  24. 3
      src/Avalonia.Controls/TreeView.cs
  25. 9
      src/Avalonia.Controls/TreeViewItem.cs
  26. 1
      src/Avalonia.Controls/Window.cs
  27. 18
      src/Avalonia.Input/FocusManager.cs
  28. 4
      src/Avalonia.Input/InputElement.cs
  29. 8
      src/Avalonia.Input/Pointer.cs
  30. 27
      src/Avalonia.Layout/LayoutHelper.cs
  31. 8
      src/Avalonia.Layout/Layoutable.cs
  32. 33
      src/Avalonia.Native/WindowImplBase.cs
  33. 13
      src/Avalonia.Styling/IDataContextProvider.cs
  34. 8
      src/Avalonia.Styling/IStyledElement.cs
  35. 4
      src/Avalonia.Styling/StyledElement.cs
  36. 5
      src/Avalonia.Themes.Default/RadioButton.xaml
  37. 27
      src/Avalonia.Visuals/Media/FontFamily.cs
  38. 34
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  39. 22
      src/Avalonia.Visuals/Visual.cs
  40. 155
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  41. 2
      src/Avalonia.X11/X11Clipboard.cs
  42. 7
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  43. 11
      src/Markup/Avalonia.Markup/Data/Binding.cs
  44. 16
      src/Markup/Avalonia.Markup/Data/MultiBinding.cs
  45. 5
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  46. 23
      src/Windows/Avalonia.Win32/OleContext.cs
  47. 2
      src/Windows/Avalonia.Win32/OleDropTarget.cs
  48. 5
      src/Windows/Avalonia.Win32/WindowImpl.cs
  49. 28
      tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs
  50. 59
      tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs
  51. 64
      tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs
  52. 16
      tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs
  53. 38
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  54. 3
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  55. 18
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  56. 30
      tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs
  57. 4
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs
  58. 172
      tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs

12
azure-pipelines.yml

@ -34,9 +34,17 @@ jobs:
pool:
vmImage: 'macOS-10.14'
steps:
- task: DotNetCoreInstaller@0
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.0.x'
inputs:
version: '2.1.403'
packageType: sdk
version: 3.0.x
- task: UseDotNet@2
displayName: 'Use .NET Core Runtime 2.1.x'
inputs:
packageType: runtime
version: 2.1.x
- task: CmdLine@2
displayName: 'Install Mono 5.18'

2
build/SharedVersion.props

@ -2,7 +2,7 @@
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Product>Avalonia</Product>
<Version>0.8.999</Version>
<Version>0.9.999</Version>
<Copyright>Copyright 2019 &#169; The AvaloniaUI Project</Copyright>
<PackageProjectUrl>https://avaloniaui.net</PackageProjectUrl>
<RepositoryUrl>https://github.com/AvaloniaUI/Avalonia/</RepositoryUrl>

4
native/Avalonia.Native/inc/avalonia-native.h

@ -212,6 +212,10 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT GetSoftwareFramebuffer(AvnFramebuffer*ret) = 0;
virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
virtual bool TryLock() = 0;
virtual void Unlock() = 0;
};

14
native/Avalonia.Native/src/OSX/app.mm

@ -1,16 +1,25 @@
#include "common.h"
@interface AvnAppDelegate : NSObject<NSApplicationDelegate>
@end
extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
@implementation AvnAppDelegate
- (void)applicationWillFinishLaunching:(NSNotification *)notification
{
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
if([[NSApplication sharedApplication] activationPolicy] != AvnDesiredActivationPolicy)
{
for (NSRunningApplication * app in [NSRunningApplication runningApplicationsWithBundleIdentifier:@"com.apple.dock"]) {
[app activateWithOptions:NSApplicationActivateIgnoringOtherApps];
break;
}
[[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
}
}
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
[NSApp activateIgnoringOtherApps:true];
[[NSRunningApplication currentApplication] activateWithOptions:NSApplicationActivateIgnoringOtherApps];
}
@end
@ -20,5 +29,4 @@ extern void InitializeAvnApp()
NSApplication* app = [NSApplication sharedApplication];
id delegate = [AvnAppDelegate new];
[app setDelegate:delegate];
}

49
native/Avalonia.Native/src/OSX/window.mm

@ -83,6 +83,54 @@ public:
[Window setContentView: View];
}
virtual HRESULT ObtainNSWindowHandle(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge void*)Window;
return S_OK;
}
virtual HRESULT ObtainNSWindowHandleRetained(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge_retained void*)Window;
return S_OK;
}
virtual HRESULT ObtainNSViewHandle(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge void*)View;
return S_OK;
}
virtual HRESULT ObtainNSViewHandleRetained(void** ret) override
{
if (ret == nullptr)
{
return E_POINTER;
}
*ret = (__bridge_retained void*)View;
return S_OK;
}
virtual AvnWindow* GetNSWindow() override
{
return Window;
@ -691,6 +739,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
{
self = [super init];
[self setWantsBestResolutionOpenGLSurface:true];
[self setWantsLayer:YES];
_parent = parent;
_area = nullptr;
return self;

4
samples/ControlCatalog/App.xaml.cs

@ -1,6 +1,4 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@ -19,7 +17,7 @@ namespace ControlCatalog
desktopLifetime.MainWindow = new MainWindow();
else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
singleViewLifetime.MainView = new MainView();
base.OnFrameworkInitializationCompleted();
}
}

12
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -224,12 +224,20 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(name != null);
if (name.Contains('.'))
if (name.Contains("."))
{
throw new InvalidOperationException("Attached properties not supported.");
}
return GetRegistered(type).FirstOrDefault(x => x.Name == name);
foreach (AvaloniaProperty x in GetRegistered(type))
{
if (x.Name == name)
{
return x;
}
}
return null;
}
/// <summary>

17
src/Avalonia.Base/Data/BindingOperations.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.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Avalonia.Reactive;
@ -57,22 +56,34 @@ namespace Avalonia.Data
if (source != null)
{
// Perf: Avoid allocating closure in the outer scope.
var targetCopy = target;
var propertyCopy = property;
var bindingCopy = binding;
return source
.Where(x => BindingNotification.ExtractValue(x) != AvaloniaProperty.UnsetValue)
.Take(1)
.Subscribe(x => target.SetValue(property, x, binding.Priority));
.Subscribe(x => targetCopy.SetValue(propertyCopy, x, bindingCopy.Priority));
}
else
{
target.SetValue(property, binding.Value, binding.Priority);
return Disposable.Empty;
}
case BindingMode.OneWayToSource:
{
// Perf: Avoid allocating closure in the outer scope.
var bindingCopy = binding;
return Observable.CombineLatest(
binding.Observable,
target.GetObservable(property),
(_, v) => v)
.Subscribe(x => binding.Subject.OnNext(x));
.Subscribe(x => bindingCopy.Subject.OnNext(x));
}
default:
throw new ArgumentException("Invalid binding mode.");
}

13
src/Avalonia.Base/Data/Core/BindingExpression.cs

@ -21,6 +21,7 @@ namespace Avalonia.Data.Core
private readonly ExpressionObserver _inner;
private readonly Type _targetType;
private readonly object _fallbackValue;
private readonly object _targetNullValue;
private readonly BindingPriority _priority;
InnerListener _innerListener;
WeakReference<object> _value;
@ -51,7 +52,7 @@ namespace Avalonia.Data.Core
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
: this(inner, targetType, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
: this(inner, targetType, AvaloniaProperty.UnsetValue, AvaloniaProperty.UnsetValue, converter, converterParameter, priority)
{
}
@ -63,6 +64,9 @@ namespace Avalonia.Data.Core
/// <param name="fallbackValue">
/// The value to use when the binding is unable to produce a value.
/// </param>
/// <param name="targetNullValue">
/// The value to use when the binding result is null.
/// </param>
/// <param name="converter">The value converter to use.</param>
/// <param name="converterParameter">
/// A parameter to pass to <paramref name="converter"/>.
@ -72,6 +76,7 @@ namespace Avalonia.Data.Core
ExpressionObserver inner,
Type targetType,
object fallbackValue,
object targetNullValue,
IValueConverter converter,
object converterParameter = null,
BindingPriority priority = BindingPriority.LocalValue)
@ -85,6 +90,7 @@ namespace Avalonia.Data.Core
Converter = converter;
ConverterParameter = converterParameter;
_fallbackValue = fallbackValue;
_targetNullValue = targetNullValue;
_priority = priority;
}
@ -196,6 +202,11 @@ namespace Avalonia.Data.Core
/// <inheritdoc/>
private object ConvertValue(object value)
{
if (value == null && _targetNullValue != AvaloniaProperty.UnsetValue)
{
return _targetNullValue;
}
if (value == BindingOperations.DoNothing)
{
return value;

6
src/Avalonia.Base/Data/Core/ExpressionObserver.cs

@ -21,7 +21,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of property accessor plugins that can be used to customize
/// the reading and subscription of property values on a type.
/// </summary>
public static readonly IList<IPropertyAccessorPlugin> PropertyAccessors =
public static readonly List<IPropertyAccessorPlugin> PropertyAccessors =
new List<IPropertyAccessorPlugin>
{
new AvaloniaPropertyAccessorPlugin(),
@ -33,7 +33,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of validation checker plugins that can be used to customize
/// the validation of view model and model data.
/// </summary>
public static readonly IList<IDataValidationPlugin> DataValidators =
public static readonly List<IDataValidationPlugin> DataValidators =
new List<IDataValidationPlugin>
{
new DataAnnotationsValidationPlugin(),
@ -45,7 +45,7 @@ namespace Avalonia.Data.Core
/// An ordered collection of stream plugins that can be used to customize the behavior
/// of the '^' stream binding operator.
/// </summary>
public static readonly IList<IStreamPlugin> StreamHandlers =
public static readonly List<IStreamPlugin> StreamHandlers =
new List<IStreamPlugin>
{
new TaskStreamPlugin(),

20
src/Avalonia.Base/Data/Core/Plugins/AvaloniaPropertyAccessorPlugin.cs

@ -2,7 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Reactive.Linq;
using System.Runtime.ExceptionServices;
namespace Avalonia.Data.Core.Plugins
{
@ -76,7 +76,7 @@ namespace Avalonia.Data.Core.Plugins
return false;
}
private class Accessor : PropertyAccessorBase
private class Accessor : PropertyAccessorBase, IObserver<object>
{
private readonly WeakReference<AvaloniaObject> _reference;
private readonly AvaloniaProperty _property;
@ -117,7 +117,7 @@ namespace Avalonia.Data.Core.Plugins
protected override void SubscribeCore()
{
_subscription = Instance?.GetObservable(_property).Subscribe(PublishValue);
_subscription = Instance?.GetObservable(_property).Subscribe(this);
}
protected override void UnsubscribeCore()
@ -125,6 +125,20 @@ namespace Avalonia.Data.Core.Plugins
_subscription?.Dispose();
_subscription = null;
}
void IObserver<object>.OnCompleted()
{
}
void IObserver<object>.OnError(Exception error)
{
ExceptionDispatchInfo.Capture(error).Throw();
}
void IObserver<object>.OnNext(object value)
{
PublishValue(value);
}
}
}
}

14
src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs

@ -2,8 +2,6 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Data.Core.Plugins;
namespace Avalonia.Data.Core
@ -41,7 +39,17 @@ namespace Avalonia.Data.Core
{
reference.TryGetTarget(out object target);
var plugin = ExpressionObserver.PropertyAccessors.FirstOrDefault(x => x.Match(target, PropertyName));
IPropertyAccessorPlugin plugin = null;
foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors)
{
if (x.Match(target, PropertyName))
{
plugin = x;
break;
}
}
var accessor = plugin?.Start(reference, PropertyName);
if (_enableValidation && Next == null)

15
src/Avalonia.Base/Platform/IMacOSTopLevelPlatformHandle.cs

@ -0,0 +1,15 @@
// 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;
namespace Avalonia.Platform
{
public interface IMacOSTopLevelPlatformHandle
{
IntPtr NSView { get; }
IntPtr GetNSViewRetained();
IntPtr NSWindow { get; }
IntPtr GetNSWindowRetained();
}
}

25
src/Avalonia.Base/Reactive/LightweightObservableBase.cs

@ -116,20 +116,33 @@ namespace Avalonia.Reactive
{
if (Volatile.Read(ref _observers) != null)
{
IObserver<T>[] observers;
IObserver<T>[] observers = null;
IObserver<T> singleObserver = null;
lock (this)
{
if (_observers == null)
{
return;
}
observers = _observers.ToArray();
if (_observers.Count == 1)
{
singleObserver = _observers[0];
}
else
{
observers = _observers.ToArray();
}
}
foreach (var observer in observers)
if (singleObserver != null)
{
observer.OnNext(value);
singleObserver.OnNext(value);
}
else
{
foreach (var observer in observers)
{
observer.OnNext(value);
}
}
}
}

2
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -15,7 +15,7 @@ namespace Avalonia.Utilities
{
if (IsValidIdentifierStart(r.Peek))
{
return r.TakeWhile(IsValidIdentifierChar);
return r.TakeWhile(c => IsValidIdentifierChar(c));
}
else
{

21
src/Avalonia.Controls/Application.cs

@ -32,7 +32,7 @@ namespace Avalonia
/// method.
/// - Tracks the lifetime of the application.
/// </remarks>
public class Application : AvaloniaObject, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
public class Application : AvaloniaObject, IDataContextProvider, IGlobalDataTemplates, IGlobalStyles, IStyleRoot, IResourceNode
{
/// <summary>
/// The application-global data templates.
@ -45,6 +45,12 @@ namespace Avalonia
private Styles _styles;
private IResourceDictionary _resources;
/// <summary>
/// Defines the <see cref="DataContext"/> property.
/// </summary>
public static readonly StyledProperty<object> DataContextProperty =
StyledElement.DataContextProperty.AddOwner<Application>();
/// <inheritdoc/>
public event EventHandler<ResourcesChangedEventArgs> ResourcesChanged;
@ -56,6 +62,19 @@ namespace Avalonia
Name = "Avalonia Application";
}
/// <summary>
/// Gets or sets the Applications's data context.
/// </summary>
/// <remarks>
/// The data context property specifies the default object that will
/// be used for data binding.
/// </remarks>
public object DataContext
{
get { return GetValue(DataContextProperty); }
set { SetValue(DataContextProperty, value); }
}
/// <summary>
/// Gets the current instance of the <see cref="Application"/> class.
/// </summary>

5
src/Avalonia.Controls/Generators/ITreeItemContainerGenerator.cs

@ -12,5 +12,10 @@ namespace Avalonia.Controls.Generators
/// Gets the container index for the tree.
/// </summary>
TreeContainerIndex Index { get; }
/// <summary>
/// Updates the index based on the parent <see cref="TreeView"/>.
/// </summary>
void UpdateIndex();
}
}

49
src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs

@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.Generators
{
@ -15,6 +17,8 @@ namespace Avalonia.Controls.Generators
public class TreeItemContainerGenerator<T> : ItemContainerGenerator<T>, ITreeItemContainerGenerator
where T : class, IControl, new()
{
private TreeView _treeView;
/// <summary>
/// Initializes a new instance of the <see cref="TreeItemContainerGenerator{T}"/> class.
/// </summary>
@ -23,31 +27,28 @@ namespace Avalonia.Controls.Generators
/// <param name="contentTemplateProperty">The container's ContentTemplate property.</param>
/// <param name="itemsProperty">The container's Items property.</param>
/// <param name="isExpandedProperty">The container's IsExpanded property.</param>
/// <param name="index">The container index for the tree</param>
public TreeItemContainerGenerator(
IControl owner,
AvaloniaProperty contentProperty,
AvaloniaProperty contentTemplateProperty,
AvaloniaProperty itemsProperty,
AvaloniaProperty isExpandedProperty,
TreeContainerIndex index)
AvaloniaProperty isExpandedProperty)
: base(owner, contentProperty, contentTemplateProperty)
{
Contract.Requires<ArgumentNullException>(owner != null);
Contract.Requires<ArgumentNullException>(contentProperty != null);
Contract.Requires<ArgumentNullException>(itemsProperty != null);
Contract.Requires<ArgumentNullException>(isExpandedProperty != null);
Contract.Requires<ArgumentNullException>(index != null);
ItemsProperty = itemsProperty;
IsExpandedProperty = isExpandedProperty;
Index = index;
UpdateIndex();
}
/// <summary>
/// Gets the container index for the tree.
/// </summary>
public TreeContainerIndex Index { get; }
public TreeContainerIndex Index { get; private set; }
/// <summary>
/// Gets the item container's Items property.
@ -70,7 +71,7 @@ namespace Avalonia.Controls.Generators
}
else if (container != null)
{
Index.Add(item, container);
Index?.Add(item, container);
return container;
}
else
@ -92,7 +93,7 @@ namespace Avalonia.Controls.Generators
result.DataContext = item;
}
Index.Add(item, result);
Index?.Add(item, result);
return result;
}
@ -101,24 +102,50 @@ namespace Avalonia.Controls.Generators
public override IEnumerable<ItemContainerInfo> Clear()
{
var items = base.Clear();
Index.Remove(0, items);
Index?.Remove(0, items);
return items;
}
public override IEnumerable<ItemContainerInfo> Dematerialize(int startingIndex, int count)
{
Index.Remove(startingIndex, GetContainerRange(startingIndex, count));
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.Dematerialize(startingIndex, count);
}
public override IEnumerable<ItemContainerInfo> RemoveRange(int startingIndex, int count)
{
Index.Remove(startingIndex, GetContainerRange(startingIndex, count));
Index?.Remove(startingIndex, GetContainerRange(startingIndex, count));
return base.RemoveRange(startingIndex, count);
}
public override bool TryRecycle(int oldIndex, int newIndex, object item) => false;
public void UpdateIndex()
{
if (Owner is TreeView treeViewOwner && Index == null)
{
Index = new TreeContainerIndex();
_treeView = treeViewOwner;
}
else if (Owner.IsAttachedToLogicalTree)
{
var treeView = Owner.GetSelfAndLogicalAncestors().OfType<TreeView>().FirstOrDefault();
if (treeView != _treeView)
{
Clear();
Index = treeView?.ItemContainerGenerator?.Index;
_treeView = treeView;
}
}
else
{
Clear();
Index = null;
_treeView = null;
}
}
class WrapperTreeDataTemplate : ITreeDataTemplate
{
private readonly IDataTemplate _inner;

25
src/Avalonia.Controls/ItemsControl.cs

@ -5,7 +5,6 @@ using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
@ -324,20 +323,24 @@ namespace Avalonia.Controls
return;
}
var current = focus.Current
.GetSelfAndVisualAncestors()
.OfType<IInputElement>()
.FirstOrDefault(x => x.VisualParent == container);
IVisual current = focus.Current;
if (current != null)
while (current != null)
{
var next = GetNextControl(container, direction.Value, current, false);
if (next != null)
if (current.VisualParent == container && current is IInputElement inputElement)
{
focus.Focus(next, NavigationMethod.Directional);
e.Handled = true;
IInputElement next = GetNextControl(container, direction.Value, inputElement, false);
if (next != null)
{
focus.Focus(next, NavigationMethod.Directional);
e.Handled = true;
}
break;
}
current = current.VisualParent;
}
}

10
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -149,15 +149,9 @@ namespace Avalonia.Controls.Notifications
/// <param name="host">The <see cref="Window"/> that will be the host.</param>
private void Install(Window host)
{
var adornerLayer = host.GetVisualDescendants()
.OfType<VisualLayerManager>()
.FirstOrDefault()
?.AdornerLayer;
var adornerLayer = host.FindDescendantOfType<VisualLayerManager>()?.AdornerLayer;
if (adornerLayer != null)
{
adornerLayer.Children.Add(this);
}
adornerLayer?.Children.Add(this);
}
}
}

25
src/Avalonia.Controls/Primitives/RangeBase.cs

@ -75,7 +75,10 @@ namespace Avalonia.Controls.Primitives
set
{
ValidateDouble(value, "Minimum");
if (!ValidateDouble(value))
{
return;
}
if (IsInitialized)
{
@ -102,7 +105,10 @@ namespace Avalonia.Controls.Primitives
set
{
ValidateDouble(value, "Maximum");
if (!ValidateDouble(value))
{
return;
}
if (IsInitialized)
{
@ -129,7 +135,10 @@ namespace Avalonia.Controls.Primitives
set
{
ValidateDouble(value, "Value");
if (!ValidateDouble(value))
{
return;
}
if (IsInitialized)
{
@ -164,16 +173,12 @@ namespace Avalonia.Controls.Primitives
}
/// <summary>
/// Throws an exception if the double value is NaN or Inf.
/// Checks if the double value is not inifinity nor NaN.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="property">The name of the property being set.</param>
private static void ValidateDouble(double value, string property)
private static bool ValidateDouble(double value)
{
if (double.IsInfinity(value) || double.IsNaN(value))
{
throw new ArgumentException($"{value} is not a valid value for {property}.");
}
return !double.IsInfinity(value) || !double.IsNaN(value);
}
/// <summary>

18
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -13,7 +13,6 @@ using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Logging;
using Avalonia.Styling;
using Avalonia.VisualTree;
namespace Avalonia.Controls.Primitives
@ -269,11 +268,20 @@ namespace Avalonia.Controls.Primitives
/// <returns>The container or null if the event did not originate in a container.</returns>
protected IControl GetContainerFromEventSource(IInteractive eventSource)
{
var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
.OfType<IControl>()
.FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1);
var parent = (IVisual)eventSource;
return item;
while (parent != null)
{
if (parent is IControl control && control.LogicalParent == this
&& ItemContainerGenerator?.IndexFromContainer(control) != -1)
{
return control;
}
parent = parent.VisualParent;
}
return null;
}
/// <inheritdoc/>

14
src/Avalonia.Controls/TopLevel.cs

@ -2,9 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
@ -15,7 +13,6 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Styling;
using Avalonia.Utilities;
using Avalonia.VisualTree;
using JetBrains.Annotations;
namespace Avalonia.Controls
@ -269,6 +266,12 @@ namespace Avalonia.Controls
/// </summary>
protected virtual void HandleClosed()
{
var logicalArgs = new LogicalTreeAttachmentEventArgs(this);
((ILogical)this).NotifyDetachedFromLogicalTree(logicalArgs);
var visualArgs = new VisualTreeAttachmentEventArgs(this, this);
OnDetachedFromVisualTreeCore(visualArgs);
(this as IInputRoot).MouseDevice?.TopLevelClosed(this);
PlatformImpl = null;
OnClosed(EventArgs.Empty);
@ -296,10 +299,7 @@ namespace Avalonia.Controls
/// <param name="scaling">The window scaling.</param>
protected virtual void HandleScalingChanged(double scaling)
{
foreach (ILayoutable control in this.GetSelfAndVisualDescendants())
{
control.InvalidateMeasure();
}
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
}
/// <inheritdoc/>

3
src/Avalonia.Controls/TreeView.cs

@ -393,8 +393,7 @@ namespace Avalonia.Controls
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
new TreeContainerIndex());
TreeViewItem.IsExpandedProperty);
result.Index.Materialized += ContainerMaterialized;
return result;
}

9
src/Avalonia.Controls/TreeViewItem.cs

@ -98,17 +98,18 @@ namespace Avalonia.Controls
TreeViewItem.HeaderProperty,
TreeViewItem.ItemTemplateProperty,
TreeViewItem.ItemsProperty,
TreeViewItem.IsExpandedProperty,
_treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
TreeViewItem.IsExpandedProperty);
}
/// <inheritdoc/>
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_treeView = this.GetLogicalAncestors().OfType<TreeView>().FirstOrDefault();
Level = CalculateDistanceFromLogicalParent<TreeView>(this) - 1;
ItemContainerGenerator.UpdateIndex();
if (ItemTemplate == null && _treeView?.ItemTemplate != null)
{
@ -119,7 +120,7 @@ namespace Avalonia.Controls
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
ItemContainerGenerator.Clear();
ItemContainerGenerator.UpdateIndex();
}
protected virtual void OnRequestBringIntoView(RequestBringIntoViewEventArgs e)

1
src/Avalonia.Controls/Window.cs

@ -336,7 +336,6 @@ namespace Avalonia.Controls
if (close)
{
PlatformImpl?.Dispose();
HandleClosed();
}
}
}

18
src/Avalonia.Input/FocusManager.cs

@ -180,18 +180,18 @@ namespace Avalonia.Input
if (sender == e.Source && ev.MouseButton == MouseButton.Left)
{
var element = (ev.Pointer?.Captured as IInputElement) ?? (e.Source as IInputElement);
IVisual element = ev.Pointer?.Captured ?? e.Source as IInputElement;
if (element == null || !CanFocus(element))
while (element != null)
{
element = element.GetSelfAndVisualAncestors()
.OfType<IInputElement>()
.FirstOrDefault(CanFocus);
}
if (element is IInputElement inputElement && CanFocus(inputElement))
{
Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers);
if (element != null)
{
Instance?.Focus(element, NavigationMethod.Pointer, ev.InputModifiers);
break;
}
element = element.VisualParent;
}
}
}

4
src/Avalonia.Input/InputElement.cs

@ -342,7 +342,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Gets or sets a value indicating whether the control is focused.
/// Gets a value indicating whether the control is focused.
/// </summary>
public bool IsFocused
{
@ -360,7 +360,7 @@ namespace Avalonia.Input
}
/// <summary>
/// Gets or sets a value indicating whether the pointer is currently over the control.
/// Gets a value indicating whether the pointer is currently over the control.
/// </summary>
public bool IsPointerOver
{

8
src/Avalonia.Input/Pointer.cs

@ -55,9 +55,11 @@ namespace Avalonia.Input
Captured.DetachedFromVisualTree += OnCaptureDetached;
}
IInputElement GetNextCapture(IVisual parent) =>
parent as IInputElement ?? parent.GetVisualAncestors().OfType<IInputElement>().FirstOrDefault();
IInputElement GetNextCapture(IVisual parent)
{
return parent as IInputElement ?? parent.FindAncestorOfType<IInputElement>();
}
private void OnCaptureDetached(object sender, VisualTreeAttachmentEventArgs e)
{
Capture(GetNextCapture(e.Parent));

27
src/Avalonia.Layout/LayoutHelper.cs

@ -2,6 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using Avalonia.VisualTree;
namespace Avalonia.Layout
{
@ -61,5 +62,31 @@ namespace Avalonia.Layout
return availableSize;
}
/// <summary>
/// Invalidates measure for given control and all visual children recursively.
/// </summary>
public static void InvalidateSelfAndChildrenMeasure(ILayoutable control)
{
void InnerInvalidateMeasure(IVisual target)
{
if (target is ILayoutable targetLayoutable)
{
targetLayoutable.InvalidateMeasure();
}
var visualChildren = target.VisualChildren;
var visualChildrenCount = visualChildren.Count;
for (int i = 0; i < visualChildrenCount; i++)
{
IVisual child = visualChildren[i];
InnerInvalidateMeasure(child);
}
}
InnerInvalidateMeasure(control);
}
}
}

8
src/Avalonia.Layout/Layoutable.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.Linq;
using Avalonia.Logging;
using Avalonia.VisualTree;
@ -694,12 +693,9 @@ namespace Avalonia.Layout
}
/// <inheritdoc/>
protected override sealed void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
protected sealed override void OnVisualParentChanged(IVisual oldParent, IVisual newParent)
{
foreach (ILayoutable i in this.GetSelfAndVisualDescendants())
{
i.InvalidateMeasure();
}
LayoutHelper.InvalidateSelfAndChildrenMeasure(this);
base.OnVisualParentChanged(oldParent, newParent);
}

33
src/Avalonia.Native/WindowImplBase.cs

@ -16,6 +16,34 @@ using Avalonia.Threading;
namespace Avalonia.Native
{
public class MacOSTopLevelWindowHandle : IPlatformHandle, IMacOSTopLevelPlatformHandle
{
IAvnWindowBase _native;
public MacOSTopLevelWindowHandle(IAvnWindowBase native)
{
_native = native;
}
public IntPtr Handle => NSWindow;
public string HandleDescriptor => "NSWindow";
public IntPtr NSView => _native.ObtainNSViewHandle();
public IntPtr NSWindow => _native.ObtainNSWindowHandle();
public IntPtr GetNSViewRetained()
{
return _native.ObtainNSViewHandleRetained();
}
public IntPtr GetNSWindowRetained()
{
return _native.ObtainNSWindowHandleRetained();
}
}
public abstract class WindowBaseImpl : IWindowBaseImpl,
IFramebufferPlatformSurface
{
@ -45,6 +73,9 @@ namespace Avalonia.Native
protected void Init(IAvnWindowBase window, IAvnScreens screens)
{
_native = window;
Handle = new MacOSTopLevelWindowHandle(window);
_glSurface = new GlPlatformSurface(window);
Screen = new ScreenImpl(screens);
_savedLogicalSize = ClientSize;
@ -349,6 +380,6 @@ namespace Avalonia.Native
}
public IPlatformHandle Handle => new PlatformHandle(IntPtr.Zero, "NOT SUPPORTED");
public IPlatformHandle Handle { get; private set; }
}
}

13
src/Avalonia.Styling/IDataContextProvider.cs

@ -0,0 +1,13 @@
namespace Avalonia
{
/// <summary>
/// Defines an element with a data context that can be used for binding.
/// </summary>
public interface IDataContextProvider : IAvaloniaObject
{
/// <summary>
/// Gets or sets the element's data context.
/// </summary>
object DataContext { get; set; }
}
}

8
src/Avalonia.Styling/IStyledElement.cs

@ -10,7 +10,8 @@ namespace Avalonia
IStyleHost,
ILogical,
IResourceProvider,
IResourceNode
IResourceNode,
IDataContextProvider
{
/// <summary>
/// Occurs when the control has finished initialization.
@ -27,11 +28,6 @@ namespace Avalonia
/// </summary>
new Classes Classes { get; set; }
/// <summary>
/// Gets or sets the control's data context.
/// </summary>
object DataContext { get; set; }
/// <summary>
/// Gets the control's logical parent.
/// </summary>

4
src/Avalonia.Styling/StyledElement.cs

@ -24,7 +24,7 @@ namespace Avalonia
/// - Implements <see cref="ILogical"/> to form part of a logical tree.
/// - A collection of class strings for custom styling.
/// </summary>
public class StyledElement : Animatable, IStyledElement, ISetLogicalParent, ISetInheritanceParent
public class StyledElement : Animatable, IDataContextProvider, IStyledElement, ISetLogicalParent, ISetInheritanceParent
{
/// <summary>
/// Defines the <see cref="DataContext"/> property.
@ -288,7 +288,7 @@ namespace Avalonia
var list = new AvaloniaList<ILogical>
{
ResetBehavior = ResetBehavior.Remove,
Validate = ValidateLogicalChild
Validate = logical => ValidateLogicalChild(logical)
};
list.CollectionChanged += LogicalChildrenCollectionChanged;
_logicalChildren = list;

5
src/Avalonia.Themes.Default/RadioButton.xaml

@ -13,7 +13,6 @@
Height="18"
VerticalAlignment="Center"/>
<Ellipse Name="checkMark"
Fill="{DynamicResource HighlightBrush}"
Width="10"
Height="10"
Stretch="Uniform"
@ -38,10 +37,12 @@
</ControlTemplate>
</Setter>
</Style>
<Style Selector="RadioButton:pointerover /template/ Ellipse#border">
<Setter Property="Stroke" Value="{DynamicResource ThemeBorderHighBrush}"/>
</Style>
<Style Selector="RadioButton /template/ Ellipse#checkMark">
<Setter Property="Fill" Value="{DynamicResource HighlightBrush}"/>
<Setter Property="IsVisible" Value="False"/>
</Style>
<Style Selector="RadioButton /template/ Ellipse#indeterminateMark">
@ -56,4 +57,4 @@
<Style Selector="RadioButton:disabled /template/ Ellipse#border">
<Setter Property="Opacity" Value="{DynamicResource ThemeDisabledOpacity}"/>
</Style>
</Styles>
</Styles>

27
src/Avalonia.Visuals/Media/FontFamily.cs

@ -184,36 +184,25 @@ namespace Avalonia.Media
{
unchecked
{
var hash = (int)2186146271;
if (Key != null)
{
hash = (hash * 15768619) ^ Key.GetHashCode();
}
else
{
hash = (hash * 15768619) ^ FamilyNames.GetHashCode();
}
if (Key != null)
{
hash = (hash * 15768619) ^ Key.GetHashCode();
}
return hash;
return ((FamilyNames != null ? FamilyNames.GetHashCode() : 0) * 397) ^ (Key != null ? Key.GetHashCode() : 0);
}
}
public override bool Equals(object obj)
{
if (ReferenceEquals(this, obj))
{
return true;
}
if (!(obj is FontFamily other))
{
return false;
}
if (Key != null)
if (!Equals(Key, other.Key))
{
return other.FamilyNames.Equals(FamilyNames) && other.Key.Equals(Key);
return false;
}
return other.FamilyNames.Equals(FamilyNames);

34
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@ -111,7 +111,24 @@ namespace Avalonia.Media.Fonts
/// </returns>
public override int GetHashCode()
{
return ToString().GetHashCode();
if (Count == 0)
{
return 0;
}
unchecked
{
int hash = 17;
for (var i = 0; i < Names.Count; i++)
{
string name = Names[i];
hash = hash * 23 + name.GetHashCode();
}
return hash;
}
}
/// <summary>
@ -128,7 +145,20 @@ namespace Avalonia.Media.Fonts
return false;
}
return other.ToString().Equals(ToString());
if (other.Count != Count)
{
return false;
}
for (int i = 0; i < Count; i++)
{
if (Names[i] != other.Names[i])
{
return false;
}
}
return true;
}
public int Count => Names.Count;

22
src/Avalonia.Visuals/Visual.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Collections;
using Avalonia.Data;
using Avalonia.Logging;
@ -121,7 +120,7 @@ namespace Avalonia
{
var visualChildren = new AvaloniaList<IVisual>();
visualChildren.ResetBehavior = ResetBehavior.Remove;
visualChildren.Validate = ValidateVisualChild;
visualChildren.Validate = visual => ValidateVisualChild(visual);
visualChildren.CollectionChanged += VisualChildrenChanged;
VisualChildren = visualChildren;
}
@ -173,7 +172,22 @@ namespace Avalonia
/// </summary>
public bool IsEffectivelyVisible
{
get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); }
get
{
IVisual node = this;
while (node != null)
{
if (!node.IsVisible)
{
return false;
}
node = node.VisualParent;
}
return true;
}
}
/// <summary>
@ -556,7 +570,7 @@ namespace Avalonia
if (_visualParent is IRenderRoot || _visualParent?.IsAttachedToVisualTree == true)
{
var root = this.GetVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var root = this.FindAncestorOfType<IRenderRoot>();
var e = new VisualTreeAttachmentEventArgs(_visualParent, root);
OnAttachedToVisualTreeCore(e);
}

155
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -14,7 +14,7 @@ namespace Avalonia.VisualTree
public static class VisualExtensions
{
/// <summary>
/// Calculates the distance from a visual's <see cref="IRenderRoot"/>.
/// Calculates the distance from a visual's ancestor.
/// </summary>
/// <param name="visual">The visual.</param>
/// <param name="ancestor">The ancestor visual.</param>
@ -30,13 +30,39 @@ namespace Avalonia.VisualTree
while (visual != null && visual != ancestor)
{
++result;
visual = visual.VisualParent;
result++;
}
return visual != null ? result : -1;
}
/// <summary>
/// Calculates the distance from a visual's root.
/// </summary>
/// <param name="visual">The visual.</param>
/// <returns>
/// The number of steps from the visual to the root.
/// </returns>
public static int CalculateDistanceFromRoot(IVisual visual)
{
Contract.Requires<ArgumentNullException>(visual != null);
var result = 0;
visual = visual?.VisualParent;
while (visual != null)
{
visual = visual.VisualParent;
result++;
}
return result;
}
/// <summary>
/// Tries to get the first common ancestor of two visuals.
/// </summary>
@ -47,8 +73,53 @@ namespace Avalonia.VisualTree
{
Contract.Requires<ArgumentNullException>(visual != null);
return visual.GetSelfAndVisualAncestors().Intersect(target.GetSelfAndVisualAncestors())
.FirstOrDefault();
if (target is null)
{
return null;
}
void GoUpwards(ref IVisual node, int count)
{
for (int i = 0; i < count; ++i)
{
node = node.VisualParent;
}
}
// We want to find lowest node first, then make sure that both nodes are at the same height.
// By doing that we can sometimes find out that other node is our lowest common ancestor.
var firstHeight = CalculateDistanceFromRoot(visual);
var secondHeight = CalculateDistanceFromRoot(target);
if (firstHeight > secondHeight)
{
GoUpwards(ref visual, firstHeight - secondHeight);
}
else
{
GoUpwards(ref target, secondHeight - firstHeight);
}
if (visual == target)
{
return visual;
}
while (visual != null && target != null)
{
IVisual firstParent = visual.VisualParent;
IVisual secondParent = target.VisualParent;
if (firstParent == secondParent)
{
return firstParent;
}
visual = visual.VisualParent;
target = target.VisualParent;
}
return null;
}
/// <summary>
@ -69,6 +140,57 @@ namespace Avalonia.VisualTree
}
}
/// <summary>
/// Finds first ancestor of given type.
/// </summary>
/// <typeparam name="T">Ancestor type.</typeparam>
/// <param name="visual">The visual.</param>
/// <param name="includeSelf">If given visual should be included in search.</param>
/// <returns>First ancestor of given type.</returns>
public static T FindAncestorOfType<T>(this IVisual visual, bool includeSelf = false) where T : class
{
if (visual is null)
{
return null;
}
IVisual parent = includeSelf ? visual : visual.VisualParent;
while (parent != null)
{
if (parent is T result)
{
return result;
}
parent = parent.VisualParent;
}
return null;
}
/// <summary>
/// Finds first descendant of given type.
/// </summary>
/// <typeparam name="T">Descendant type.</typeparam>
/// <param name="visual">The visual.</param>
/// <param name="includeSelf">If given visual should be included in search.</param>
/// <returns>First descendant of given type.</returns>
public static T FindDescendantOfType<T>(this IVisual visual, bool includeSelf = false) where T : class
{
if (visual is null)
{
return null;
}
if (includeSelf && visual is T result)
{
return result;
}
return FindDescendantOfTypeCore<T>(visual);
}
/// <summary>
/// Enumerates an <see cref="IVisual"/> and its ancestors in the visual tree.
/// </summary>
@ -249,6 +371,31 @@ namespace Avalonia.VisualTree
.Select(x => x.Element);
}
private static T FindDescendantOfTypeCore<T>(IVisual visual) where T : class
{
var visualChildren = visual.VisualChildren;
var visualChildrenCount = visualChildren.Count;
for (var i = 0; i < visualChildrenCount; i++)
{
IVisual child = visualChildren[i];
if (child is T result)
{
return result;
}
var childResult = FindDescendantOfTypeCore<T>(child);
if (!(childResult is null))
{
return childResult;
}
}
return null;
}
private class ZOrderElement : IComparable<ZOrderElement>
{
public IVisual Element { get; set; }

2
src/Avalonia.X11/X11Clipboard.cs

@ -79,7 +79,7 @@ namespace Avalonia.X11
atoms = atoms.Concat(new[] {_x11.Atoms.TARGETS, _x11.Atoms.MULTIPLE})
.ToArray();
XChangeProperty(_x11.Display, window, property,
target, 32, PropertyMode.Replace, atoms, atoms.Length);
_x11.Atoms.XA_ATOM, 32, PropertyMode.Replace, atoms, atoms.Length);
return property;
}
else if(target == _x11.Atoms.SAVE_TARGETS && _x11.Atoms.SAVE_TARGETS != IntPtr.Zero)

7
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@ -52,6 +52,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
// the context.
object anchor = context.GetFirstParent<IControl>();
if(anchor is null)
{
// Try to find IDataContextProvider, this was added to allow us to find
// a datacontext for Application class when using NativeMenuItems.
anchor = context.GetFirstParent<IDataContextProvider>();
}
// If a control was not found, then try to find the highest-level style as the XAML
// file could be a XAML file containing only styles.
return anchor ??

11
src/Markup/Avalonia.Markup/Data/Binding.cs

@ -26,6 +26,7 @@ namespace Avalonia.Data
public Binding()
{
FallbackValue = AvaloniaProperty.UnsetValue;
TargetNullValue = AvaloniaProperty.UnsetValue;
}
/// <summary>
@ -60,6 +61,11 @@ namespace Avalonia.Data
/// </summary>
public object FallbackValue { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object TargetNullValue { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
@ -209,6 +215,7 @@ namespace Avalonia.Data
observer,
targetType,
fallback,
TargetNullValue,
converter ?? DefaultValueConverter.Instance,
ConverterParameter,
Priority);
@ -224,9 +231,9 @@ namespace Avalonia.Data
{
Contract.Requires<ArgumentNullException>(target != null);
if (!(target is IStyledElement))
if (!(target is IDataContextProvider))
{
target = anchor as IStyledElement;
target = anchor as IDataContextProvider;
if (target == null)
{

16
src/Markup/Avalonia.Markup/Data/MultiBinding.cs

@ -37,6 +37,11 @@ namespace Avalonia.Data
/// </summary>
public object FallbackValue { get; set; }
/// <summary>
/// Gets or sets the value to use when the binding result is null.
/// </summary>
public object TargetNullValue { get; set; }
/// <summary>
/// Gets or sets the binding mode.
/// </summary>
@ -57,6 +62,12 @@ namespace Avalonia.Data
/// </summary>
public string StringFormat { get; set; }
public MultiBinding()
{
FallbackValue = AvaloniaProperty.UnsetValue;
TargetNullValue = AvaloniaProperty.UnsetValue;
}
/// <inheritdoc/>
public InstancedBinding Initiate(
IAvaloniaObject target,
@ -102,6 +113,11 @@ namespace Avalonia.Data
var culture = CultureInfo.CurrentCulture;
var converted = converter.Convert(values, targetType, ConverterParameter, culture);
if (converted == null)
{
converted = TargetNullValue;
}
if (converted == AvaloniaProperty.UnsetValue)
{
converted = FallbackValue;

5
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1098,7 +1098,10 @@ namespace Avalonia.Win32.Interop
[DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern HRESULT RegisterDragDrop(IntPtr hwnd, IDropTarget target);
[DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
public static extern HRESULT RevokeDragDrop(IntPtr hwnd);
[DllImport("ole32.dll", EntryPoint = "OleInitialize")]
public static extern HRESULT OleInitialize(IntPtr val);

23
src/Windows/Avalonia.Win32/OleContext.cs

@ -7,9 +7,9 @@ using Avalonia.Win32.Interop;
namespace Avalonia.Win32
{
class OleContext
internal class OleContext
{
private static OleContext fCurrent;
private static OleContext s_current;
internal static OleContext Current
{
@ -18,13 +18,12 @@ namespace Avalonia.Win32
if (!IsValidOleThread())
return null;
if (fCurrent == null)
fCurrent = new OleContext();
return fCurrent;
if (s_current == null)
s_current = new OleContext();
return s_current;
}
}
private OleContext()
{
UnmanagedMethods.HRESULT res = UnmanagedMethods.OleInitialize(IntPtr.Zero);
@ -43,9 +42,21 @@ namespace Avalonia.Win32
internal bool RegisterDragDrop(IPlatformHandle hwnd, IDropTarget target)
{
if (hwnd?.HandleDescriptor != "HWND" || target == null)
{
return false;
}
return UnmanagedMethods.RegisterDragDrop(hwnd.Handle, target) == UnmanagedMethods.HRESULT.S_OK;
}
internal bool UnregisterDragDrop(IPlatformHandle hwnd)
{
if (hwnd?.HandleDescriptor != "HWND")
{
return false;
}
return UnmanagedMethods.RevokeDragDrop(hwnd.Handle) == UnmanagedMethods.HRESULT.S_OK;
}
}
}

2
src/Windows/Avalonia.Win32/OleDropTarget.cs

@ -6,7 +6,7 @@ using IDataObject = Avalonia.Input.IDataObject;
namespace Avalonia.Win32
{
class OleDropTarget : IDropTarget
internal class OleDropTarget : IDropTarget
{
private readonly IInputRoot _target;
private readonly ITopLevelImpl _tl;

5
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -253,6 +253,11 @@ namespace Avalonia.Win32
public void Dispose()
{
if (_dropTarget != null)
{
OleContext.Current?.UnregisterDragDrop(Handle);
_dropTarget = null;
}
if (_hwnd != IntPtr.Zero)
{
UnmanagedMethods.DestroyWindow(_hwnd);

28
tests/Avalonia.Base.UnitTests/Data/Core/BindingExpressionTests.cs

@ -139,6 +139,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
ExpressionObserver.Create(data, o => o.StringValue),
typeof(int),
42,
AvaloniaProperty.UnsetValue,
DefaultValueConverter.Instance);
var result = await target.Take(1);
@ -160,6 +161,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
ExpressionObserver.Create(data, o => o.StringValue, true),
typeof(int),
42,
AvaloniaProperty.UnsetValue,
DefaultValueConverter.Instance);
var result = await target.Take(1);
@ -181,6 +183,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
ExpressionObserver.Create(data, o => o.StringValue),
typeof(int),
"bar",
AvaloniaProperty.UnsetValue,
DefaultValueConverter.Instance);
var result = await target.Take(1);
@ -203,6 +206,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
ExpressionObserver.Create(data, o => o.StringValue, true),
typeof(int),
"bar",
AvaloniaProperty.UnsetValue,
DefaultValueConverter.Instance);
var result = await target.Take(1);
@ -238,6 +242,7 @@ namespace Avalonia.Base.UnitTests.Data.Core
ExpressionObserver.Create(data, o => o.DoubleValue),
typeof(string),
"9.8",
AvaloniaProperty.UnsetValue,
DefaultValueConverter.Instance);
target.OnNext("foo");
@ -353,6 +358,29 @@ namespace Avalonia.Base.UnitTests.Data.Core
GC.KeepAlive(data);
}
[Fact]
public async Task Null_Value_Should_Use_TargetNullValue()
{
var data = new Class1 { StringValue = "foo" };
var target = new BindingExpression(
ExpressionObserver.Create(data, o => o.StringValue),
typeof(string),
AvaloniaProperty.UnsetValue,
"bar",
DefaultValueConverter.Instance);
object result = null;
target.Subscribe(x => result = x);
Assert.Equal("foo", result);
data.StringValue = null;
Assert.Equal("bar", result);
GC.KeepAlive(data);
}
private class Class1 : NotifyingBase
{
private string _stringValue;

59
tests/Avalonia.Benchmarks/Data/BindingsBenchmark.cs

@ -0,0 +1,59 @@
using Avalonia.Data;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Data
{
[MemoryDiagnoser, InProcess]
public class BindingsBenchmark
{
[Benchmark]
public void TwoWayBinding_Via_Binding()
{
var instance = new TestClass();
var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay)
{
Source = instance
};
instance.Bind(TestClass.IntValueProperty, binding);
}
[Benchmark]
public void UpdateTwoWayBinding_Via_Binding()
{
var instance = new TestClass();
var binding = new Binding(nameof(TestClass.BoundValue), BindingMode.TwoWay)
{
Source = instance
};
instance.Bind(TestClass.IntValueProperty, binding);
for (int i = 0; i < 60; i++)
{
instance.IntValue = i;
}
}
private class TestClass : AvaloniaObject
{
public static readonly StyledProperty<int> IntValueProperty =
AvaloniaProperty.Register<TestClass, int>(nameof(IntValue));
public static readonly StyledProperty<int> BoundValueProperty =
AvaloniaProperty.Register<TestClass, int>(nameof(BoundValue));
public int IntValue
{
get => GetValue(IntValueProperty);
set => SetValue(IntValueProperty, value);
}
public int BoundValue
{
get => GetValue(BoundValueProperty);
set => SetValue(BoundValueProperty, value);
}
}
}
}

64
tests/Avalonia.Benchmarks/Traversal/VisualTreeTraversal.cs

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using BenchmarkDotNet.Attributes;
namespace Avalonia.Benchmarks.Traversal
{
[MemoryDiagnoser]
public class VisualTreeTraversal
{
private readonly TestRoot _root;
private readonly List<Control> _controls = new List<Control>();
private readonly List<Control> _shuffledControls;
public VisualTreeTraversal()
{
var panel = new StackPanel();
_root = new TestRoot { Child = panel, Renderer = new NullRenderer()};
_controls.Add(panel);
_controls = ControlHierarchyCreator.CreateChildren(_controls, panel, 3, 5, 4);
var random = new Random(1);
_shuffledControls = _controls.OrderBy(r => random.Next()).ToList();
_root.LayoutManager.ExecuteInitialLayoutPass(_root);
}
[Benchmark]
public void FindAncestorOfType_Linq()
{
foreach (Control control in _controls)
{
control.GetSelfAndVisualAncestors()
.OfType<TestRoot>()
.FirstOrDefault();
}
}
[Benchmark]
public void FindAncestorOfType_Optimized()
{
foreach (Control control in _controls)
{
control.FindAncestorOfType<TestRoot>();
}
}
[Benchmark]
public void FindCommonVisualAncestor()
{
foreach (IVisual first in _controls)
{
foreach (Control second in _shuffledControls)
{
first.FindCommonVisualAncestor(second);
}
}
}
}
}

16
tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs

@ -82,22 +82,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
Assert.Equal(50, target.Value);
}
[Fact]
public void Properties_Should_Not_Accept_Nan_And_Inifinity()
{
var target = new TestRange();
Assert.Throws<ArgumentException>(() => target.Minimum = double.NaN);
Assert.Throws<ArgumentException>(() => target.Minimum = double.PositiveInfinity);
Assert.Throws<ArgumentException>(() => target.Minimum = double.NegativeInfinity);
Assert.Throws<ArgumentException>(() => target.Maximum = double.NaN);
Assert.Throws<ArgumentException>(() => target.Maximum = double.PositiveInfinity);
Assert.Throws<ArgumentException>(() => target.Maximum = double.NegativeInfinity);
Assert.Throws<ArgumentException>(() => target.Value = double.NaN);
Assert.Throws<ArgumentException>(() => target.Value = double.PositiveInfinity);
Assert.Throws<ArgumentException>(() => target.Value = double.NegativeInfinity);
}
[Theory]
[InlineData(true)]
[InlineData(false)]

38
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
@ -33,6 +34,8 @@ namespace Avalonia.Controls.UnitTests
Items = CreateTestTreeData(),
};
var root = new TestRoot(target);
CreateNodeDataTemplate(target);
ApplyTemplates(target);
@ -77,6 +80,8 @@ namespace Avalonia.Controls.UnitTests
Items = CreateTestTreeData(),
};
var root = new TestRoot(target);
CreateNodeDataTemplate(target);
ApplyTemplates(target);
@ -527,6 +532,8 @@ namespace Avalonia.Controls.UnitTests
Items = data,
};
var root = new TestRoot(target);
CreateNodeDataTemplate(target);
ApplyTemplates(target);
@ -893,6 +900,37 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(2, GetItem(target, 0, 1, 0).Level);
}
[Fact]
public void Adding_Node_To_Removed_And_ReAdded_Parent_Should_Not_Crash()
{
// Issue #2985
var tree = CreateTestTreeData();
var target = new TreeView
{
Template = CreateTreeViewTemplate(),
Items = tree,
};
var visualRoot = new TestRoot();
visualRoot.Child = target;
CreateNodeDataTemplate(target);
ApplyTemplates(target);
ExpandAll(target);
var parent = tree[0];
var node = parent.Children[1];
parent.Children.Remove(node);
parent.Children.Add(node);
var item = target.ItemContainerGenerator.Index.ContainerFromItem(node);
ApplyTemplates(new[] { item });
// #2985 causes ArgumentException here.
node.Children.Add(new Node());
}
[Fact]
public void Auto_Expanding_In_Style_Should_Not_Break_Range_Selection()
{

3
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -228,8 +228,7 @@ namespace Avalonia.Controls.UnitTests
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windowImpl = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
var target = new Window(windowImpl);
var target = new Window();
target.Show();
target.Close();

18
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -405,6 +405,24 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal(42, target.Value);
}
[Fact]
public void Should_Return_TargetNullValue_When_Value_Is_Null()
{
var target = new TextBlock();
var source = new Source { Foo = null };
var binding = new Binding
{
Source = source,
Path = "Foo",
TargetNullValue = "(null)",
};
target.Bind(TextBlock.TextProperty, binding);
Assert.Equal("(null)", target.Text);
}
[Fact]
public void Null_Path_Should_Bind_To_DataContext()
{

30
tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests.cs

@ -94,6 +94,28 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("fallback", target.Text);
}
[Fact]
public void Should_Return_TargetNullValue_When_Value_Is_Null()
{
var target = new TextBlock();
var binding = new MultiBinding
{
Converter = new NullValueConverter(),
Bindings = new[]
{
new Binding { Path = "A" },
new Binding { Path = "B" },
new Binding { Path = "C" },
},
TargetNullValue = "(null)",
};
target.Bind(TextBlock.TextProperty, binding);
Assert.Equal("(null)", target.Text);
}
private class ConcatConverter : IMultiValueConverter
{
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
@ -109,5 +131,13 @@ namespace Avalonia.Markup.UnitTests.Data
return AvaloniaProperty.UnsetValue;
}
}
private class NullValueConverter : IMultiValueConverter
{
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture)
{
return null;
}
}
}
}

4
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -28,6 +28,10 @@ namespace Avalonia.UnitTests
return CreatePopupMock().Object;
});
mock.Setup(x => x.Dispose()).Callback(() =>
{
mock.Object.Closed?.Invoke();
});
PixelPoint pos = default;
mock.SetupGet(x => x.Position).Returns(() => pos);
mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));

172
tests/Avalonia.Visuals.UnitTests/VisualExtensionsTests.cs

@ -2,12 +2,184 @@
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Visuals.UnitTests
{
public class VisualExtensionsTests
{
[Fact]
public void FindAncestorOfType_Finds_Direct_Parent()
{
StackPanel target;
var root = new TestRoot
{
Child = target = new StackPanel()
};
Assert.Equal(root, target.FindAncestorOfType<TestRoot>());
}
[Fact]
public void FindAncestorOfType_Finds_Ancestor_Of_Nested_Child()
{
Button target;
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
new StackPanel
{
Children =
{
(target = new Button())
}
}
}
}
};
Assert.Equal(root, target.FindAncestorOfType<TestRoot>());
}
[Fact]
public void FindDescendantOfType_Finds_Direct_Child()
{
StackPanel target;
var root = new TestRoot
{
Child = target = new StackPanel()
};
Assert.Equal(target, root.FindDescendantOfType<StackPanel>());
}
[Fact]
public void FindDescendantOfType_Finds_Nested_Child()
{
Button target;
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
new StackPanel
{
Children =
{
(target = new Button())
}
}
}
}
};
Assert.Equal(target, root.FindDescendantOfType<Button>());
}
[Fact]
public void FindCommonVisualAncestor_First_Is_Parent_Of_Second()
{
Control left, right;
var root = new TestRoot
{
Child = left = new Decorator
{
Child = right = new Decorator()
}
};
var ancestor = left.FindCommonVisualAncestor(right);
Assert.Equal(left, ancestor);
ancestor = right.FindCommonVisualAncestor(left);
Assert.Equal(left, ancestor);
}
[Fact]
public void FindCommonVisualAncestor_Two_Subtrees_Uniform_Height()
{
Control left, right;
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
new Decorator
{
Child = new Decorator
{
Child = left = new Decorator()
}
},
new Decorator
{
Child = new Decorator
{
Child = right = new Decorator()
}
}
}
}
};
var ancestor = left.FindCommonVisualAncestor(right);
Assert.Equal(root.Child, ancestor);
ancestor = right.FindCommonVisualAncestor(left);
Assert.Equal(root.Child, ancestor);
}
[Fact]
public void FindCommonVisualAncestor_Two_Subtrees_NonUniform_Height()
{
Control left, right;
var root = new TestRoot
{
Child = new StackPanel
{
Children =
{
new Decorator
{
Child = new Decorator
{
Child = left = new Decorator()
}
},
new Decorator
{
Child = new Decorator
{
Child = new Decorator
{
Child = right = new Decorator()
}
}
}
}
}
};
var ancestor = left.FindCommonVisualAncestor(right);
Assert.Equal(root.Child, ancestor);
ancestor = right.FindCommonVisualAncestor(left);
Assert.Equal(root.Child, ancestor);
}
[Fact]
public void TranslatePoint_Should_Respect_RenderTransforms()
{

Loading…
Cancel
Save