Browse Source

Merge branch 'master' into fixes/1299-devtools-stuck-adorners

pull/1300/head
Steven Kirk 9 years ago
committed by GitHub
parent
commit
f70508fcb8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      src/Avalonia.Base/Collections/AvaloniaList.cs
  2. 15
      src/Avalonia.Base/Collections/IAvaloniaList.cs
  3. 2
      src/Avalonia.Controls/Calendar/Calendar.cs
  4. 1
      src/Avalonia.Controls/Control.cs
  5. 33
      src/Avalonia.Controls/DropDown.cs
  6. 11
      src/Avalonia.Controls/ItemsControl.cs
  7. 11
      src/Avalonia.Controls/Panel.cs
  8. 2
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  9. 3
      src/Avalonia.Controls/ProgressBar.cs
  10. 5
      src/Avalonia.Controls/TreeView.cs
  11. 8
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  12. 2
      src/Avalonia.Visuals/Media/PathMarkupParser.cs
  13. 2
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  14. 13
      src/Avalonia.Visuals/Visual.cs
  15. 109
      src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs
  16. 2
      src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs
  17. 153
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  18. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs
  19. 91
      src/Markup/Avalonia.Markup/ControlLocator.cs
  20. 10
      src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs
  21. 1
      src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs
  22. 22
      tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs
  23. 63
      tests/Avalonia.Controls.UnitTests/PanelTests.cs
  24. 23
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs
  25. 32
      tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs
  26. 174
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

5
src/Avalonia.Base/Collections/AvaloniaList.cs

@ -350,14 +350,15 @@ namespace Avalonia.Collections
public void MoveRange(int oldIndex, int count, int newIndex)
{
var items = _inner.GetRange(oldIndex, count);
var modifiedNewIndex = newIndex;
_inner.RemoveRange(oldIndex, count);
if (newIndex > oldIndex)
{
newIndex -= count;
modifiedNewIndex -= count;
}
_inner.InsertRange(newIndex, items);
_inner.InsertRange(modifiedNewIndex, items);
if (_collectionChanged != null)
{

15
src/Avalonia.Base/Collections/IAvaloniaList.cs

@ -36,6 +36,21 @@ namespace Avalonia.Collections
/// <param name="items">The items.</param>
void InsertRange(int index, IEnumerable<T> items);
/// <summary>
/// Moves an item to a new index.
/// </summary>
/// <param name="oldIndex">The index of the item to move.</param>
/// <param name="newIndex">The index to move the item to.</param>
void Move(int oldIndex, int newIndex);
/// <summary>
/// Moves multiple items to a new index.
/// </summary>
/// <param name="oldIndex">The first index of the items to move.</param>
/// <param name="count">The number of items to move.</param>
/// <param name="newIndex">The index to move the items to.</param>
void MoveRange(int oldIndex, int count, int newIndex);
/// <summary>
/// Removes multiple items from the collection.
/// </summary>

2
src/Avalonia.Controls/Calendar/Calendar.cs

@ -549,7 +549,7 @@ namespace Avalonia.Controls
}
else
{
if (addedDate.HasValue && !(SelectedDates.Count > 0 && SelectedDates[0] == addedDate.Value))
if (!(SelectedDates.Count > 0 && SelectedDates[0] == addedDate.Value))
{
foreach (DateTime item in SelectedDates)
{

1
src/Avalonia.Controls/Control.cs

@ -621,7 +621,6 @@ namespace Avalonia.Controls
Contract.Requires<ArgumentNullException>(property != null);
Contract.Requires<ArgumentNullException>(selector != null);
Contract.Requires<ArgumentNullException>(className != null);
Contract.Requires<ArgumentNullException>(property != null);
if (string.IsNullOrWhiteSpace(className))
{

33
src/Avalonia.Controls/DropDown.cs

@ -96,6 +96,16 @@ namespace Avalonia.Controls
this.UpdateSelectionBoxItem(this.SelectedItem);
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
if (!e.Handled && e.NavigationMethod == NavigationMethod.Directional)
{
e.Handled = UpdateSelectionFromEventSource(e.Source);
}
}
/// <inheritdoc/>
protected override void OnKeyDown(KeyEventArgs e)
{
@ -104,7 +114,7 @@ namespace Avalonia.Controls
if (!e.Handled)
{
if (e.Key == Key.F4 ||
(e.Key == Key.Down && ((e.Modifiers & InputModifiers.Alt) != 0)))
((e.Key == Key.Down || e.Key == Key.Up) && ((e.Modifiers & InputModifiers.Alt) != 0)))
{
IsDropDownOpen = !IsDropDownOpen;
e.Handled = true;
@ -114,6 +124,27 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
e.Handled = true;
}
if (!IsDropDownOpen)
{
if (e.Key == Key.Down)
{
if (SelectedIndex == -1)
SelectedIndex = 0;
if (++SelectedIndex >= ItemCount)
SelectedIndex = 0;
e.Handled = true;
}
else if (e.Key == Key.Up)
{
if (--SelectedIndex < 0)
SelectedIndex = ItemCount - 1;
e.Handled = true;
}
}
}
}

11
src/Avalonia.Controls/ItemsControl.cs

@ -11,6 +11,7 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
@ -106,6 +107,12 @@ namespace Avalonia.Controls
set { SetAndRaise(ItemsProperty, ref _items, value); }
}
public int ItemCount
{
get;
private set;
}
/// <summary>
/// Gets or sets the panel used to display the items.
/// </summary>
@ -352,6 +359,10 @@ namespace Avalonia.Controls
RemoveControlItemsFromLogicalChildren(e.OldItems);
break;
}
int? count = (Items as IList)?.Count;
if (count != null)
ItemCount = (int)count;
var collection = sender as ICollection;
PseudoClasses.Set(":empty", collection == null || collection.Count == 0);

11
src/Avalonia.Controls/Panel.cs

@ -115,6 +115,11 @@ namespace Avalonia.Controls
VisualChildren.AddRange(e.NewItems.OfType<Visual>());
break;
case NotifyCollectionChangedAction.Move:
LogicalChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex);
VisualChildren.MoveRange(e.OldStartingIndex, e.OldItems.Count, e.NewStartingIndex);
break;
case NotifyCollectionChangedAction.Remove:
controls = e.OldItems.OfType<Control>().ToList();
LogicalChildren.RemoveAll(controls);
@ -132,11 +137,7 @@ namespace Avalonia.Controls
break;
case NotifyCollectionChangedAction.Reset:
controls = e.OldItems.OfType<Control>().ToList();
LogicalChildren.Clear();
VisualChildren.Clear();
VisualChildren.AddRange(_children);
break;
throw new NotSupportedException();
}
InvalidateMeasure();

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

@ -297,7 +297,7 @@ namespace Avalonia.Controls.Primitives
.OfType<IControl>()
.FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1);
return item as IControl;
return item;
}
/// <inheritdoc/>

3
src/Avalonia.Controls/ProgressBar.cs

@ -28,9 +28,6 @@ namespace Avalonia.Controls
{
ValueProperty.Changed.AddClassHandler<ProgressBar>(x => x.ValueChanged);
HorizontalAlignmentProperty.OverrideDefaultValue<ProgressBar>(HorizontalAlignment.Left);
VerticalAlignmentProperty.OverrideDefaultValue<ProgressBar>(VerticalAlignment.Top);
IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
(p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
OrientationProperty.Changed.AddClassHandler<ProgressBar>(

5
src/Avalonia.Controls/TreeView.cs

@ -176,10 +176,7 @@ namespace Avalonia.Controls
SelectedItem = item;
if (SelectedItem != null)
{
MarkContainerSelected(container, true);
}
MarkContainerSelected(container, true);
}
}

8
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -134,12 +134,14 @@ namespace Avalonia.Controls
protected override IInputElement GetControlInDirection(NavigationDirection direction, IControl from)
{
if (from == null)
return null;
var logicalScrollable = Parent as ILogicalScrollable;
var fromControl = from as IControl;
if (logicalScrollable?.IsLogicalScrollEnabled == true && fromControl != null)
if (logicalScrollable?.IsLogicalScrollEnabled == true)
{
return logicalScrollable.GetControlInDirection(direction, fromControl);
return logicalScrollable.GetControlInDirection(direction, from);
}
else
{

2
src/Avalonia.Visuals/Media/PathMarkupParser.cs

@ -320,7 +320,7 @@ namespace Avalonia.Media
if (c == 'E')
{
readSign = false;
readExponent = c == 'E';
readExponent = true;
}
}
else

2
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@ -192,7 +192,7 @@ namespace Avalonia.Rendering.SceneGraph
UpdateLayer(node, scene.Layers[node.LayerRoot]);
}
}
else if (!startLayer && node.LayerRoot == node.Visual && node.Parent != null)
else if (node.LayerRoot == node.Visual && node.Parent != null)
{
ClearLayer(scene, node);
}

13
src/Avalonia.Visuals/Visual.cs

@ -537,6 +537,19 @@ namespace Avalonia
v.SetVisualParent(null);
}
break;
case NotifyCollectionChangedAction.Replace:
foreach (Visual v in e.OldItems)
{
v.SetVisualParent(null);
}
foreach (Visual v in e.NewItems)
{
v.SetVisualParent(this);
}
break;
}
}

109
src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs

@ -66,7 +66,7 @@ namespace Avalonia.Markup.Xaml.Data
/// <summary>
/// Gets or sets the binding path.
/// </summary>
public string Path { get; set; }
public string Path { get; set; } = "";
/// <summary>
/// Gets or sets the binding priority.
@ -93,53 +93,53 @@ namespace Avalonia.Markup.Xaml.Data
bool enableDataValidation = false)
{
Contract.Requires<ArgumentNullException>(target != null);
anchor = anchor ?? DefaultAnchor?.Target;
var pathInfo = ParsePath(Path);
ValidateState(pathInfo);
enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue;
ExpressionObserver observer;
if (pathInfo.ElementName != null || ElementName != null)
if (ElementName != null)
{
observer = CreateElementObserver(
(target as IControl) ?? (anchor as IControl),
pathInfo.ElementName ?? ElementName,
pathInfo.Path);
ElementName,
Path,
enableDataValidation);
}
else if (Source != null)
{
observer = CreateSourceObserver(Source, pathInfo.Path, enableDataValidation);
observer = CreateSourceObserver(Source, Path, enableDataValidation);
}
else if (RelativeSource == null || RelativeSource.Mode == RelativeSourceMode.DataContext)
{
observer = CreateDataContexObserver(
target,
pathInfo.Path,
Path,
targetProperty == Control.DataContextProperty,
anchor,
enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.Self)
{
observer = CreateSourceObserver(target, pathInfo.Path, enableDataValidation);
observer = CreateSourceObserver(target, Path, enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent)
{
observer = CreateTemplatedParentObserver(target, pathInfo.Path);
observer = CreateTemplatedParentObserver(target, Path, enableDataValidation);
}
else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor)
{
if (RelativeSource.AncestorType == null)
if (RelativeSource.Tree == TreeType.Visual && RelativeSource.AncestorType == null)
{
throw new InvalidOperationException("AncestorType must be set for RelativeSourceModel.FindAncestor.");
throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree.");
}
observer = CreateFindAncestorObserver(
(target as IControl) ?? (anchor as IControl),
pathInfo.Path);
RelativeSource,
Path,
enableDataValidation);
}
else
{
@ -168,53 +168,6 @@ namespace Avalonia.Markup.Xaml.Data
return new InstancedBinding(subject, Mode, Priority);
}
private static PathInfo ParsePath(string path)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
}
else if (path.StartsWith("#"))
{
var dot = path.IndexOf('.');
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
result.ElementName = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
result.ElementName = path.Substring(1);
}
}
else
{
result.Path = path;
}
return result;
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private ExpressionObserver CreateDataContexObserver(
IAvaloniaObject target,
string path,
@ -256,7 +209,11 @@ namespace Avalonia.Markup.Xaml.Data
}
}
private ExpressionObserver CreateElementObserver(IControl target, string elementName, string path)
private ExpressionObserver CreateElementObserver(
IControl target,
string elementName,
string path,
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -264,35 +221,39 @@ namespace Avalonia.Markup.Xaml.Data
var result = new ExpressionObserver(
ControlLocator.Track(target, elementName),
path,
false,
enableDataValidation,
description);
return result;
}
private ExpressionObserver CreateFindAncestorObserver(
IControl target,
string path)
RelativeSource relativeSource,
string path,
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(target != null);
return new ExpressionObserver(
ControlLocator.Track(target, RelativeSource.AncestorType, RelativeSource.AncestorLevel -1),
path);
ControlLocator.Track(target, relativeSource.Tree, relativeSource.AncestorLevel - 1, relativeSource.AncestorType),
path,
enableDataValidation);
}
private ExpressionObserver CreateSourceObserver(
object source,
string path,
bool enabledDataValidation)
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(source != null);
return new ExpressionObserver(source, path, enabledDataValidation);
return new ExpressionObserver(source, path, enableDataValidation);
}
private ExpressionObserver CreateTemplatedParentObserver(
IAvaloniaObject target,
string path)
string path,
bool enableDataValidation)
{
Contract.Requires<ArgumentNullException>(target != null);
@ -303,7 +264,8 @@ namespace Avalonia.Markup.Xaml.Data
var result = new ExpressionObserver(
() => target.GetValue(Control.TemplatedParentProperty),
path,
update);
update,
enableDataValidation);
return result;
}
@ -328,6 +290,7 @@ namespace Avalonia.Markup.Xaml.Data
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
}
}
}

2
src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs

@ -87,5 +87,7 @@ namespace Avalonia.Markup.Xaml.Data
/// Gets or sets a value that describes the type of relative source lookup.
/// </summary>
public RelativeSourceMode Mode { get; set; }
public TreeType Tree { get; set; } = TreeType.Visual;
}
}

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

@ -29,20 +29,167 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
public override object ProvideValue(IServiceProvider serviceProvider)
{
var descriptorContext = (ITypeDescriptorContext)serviceProvider;
var pathInfo = ParsePath(Path, descriptorContext);
ValidateState(pathInfo);
return new Binding
{
Converter = Converter,
ConverterParameter = ConverterParameter,
ElementName = ElementName,
ElementName = pathInfo.ElementName ?? ElementName,
FallbackValue = FallbackValue,
Mode = Mode,
Path = Path,
Path = pathInfo.Path,
Priority = Priority,
RelativeSource = RelativeSource,
RelativeSource = pathInfo.RelativeSource ?? RelativeSource,
DefaultAnchor = new WeakReference(GetDefaultAnchor((ITypeDescriptorContext)serviceProvider))
};
}
private class PathInfo
{
public string Path { get; set; }
public string ElementName { get; set; }
public RelativeSource RelativeSource { get; set; }
}
private void ValidateState(PathInfo pathInfo)
{
if (pathInfo.ElementName != null && ElementName != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when an #elementName path is provided.");
}
if (pathInfo.RelativeSource != null && RelativeSource != null)
{
throw new InvalidOperationException(
"ElementName property cannot be set when a $self or $parent path is provided.");
}
if ((pathInfo.ElementName != null || ElementName != null) &&
(pathInfo.RelativeSource != null || RelativeSource != null))
{
throw new InvalidOperationException(
"ElementName property cannot be set with a RelativeSource.");
}
}
private static PathInfo ParsePath(string path, ITypeDescriptorContext context)
{
var result = new PathInfo();
if (string.IsNullOrWhiteSpace(path) || path == ".")
{
result.Path = string.Empty;
}
else if (path.StartsWith("#"))
{
var dot = path.IndexOf('.');
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
result.ElementName = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
result.ElementName = path.Substring(1);
}
}
else if (path.StartsWith("$"))
{
var relativeSource = new RelativeSource
{
Tree = TreeType.Logical
};
result.RelativeSource = relativeSource;
var dot = path.IndexOf('.');
string relativeSourceMode;
if (dot != -1)
{
result.Path = path.Substring(dot + 1);
relativeSourceMode = path.Substring(1, dot - 1);
}
else
{
result.Path = string.Empty;
relativeSourceMode = path.Substring(1);
}
if (relativeSourceMode == "self")
{
relativeSource.Mode = RelativeSourceMode.Self;
}
else if (relativeSourceMode == "parent")
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
relativeSource.AncestorLevel = 1;
}
else if (relativeSourceMode.StartsWith("parent["))
{
relativeSource.Mode = RelativeSourceMode.FindAncestor;
var parentConfigStart = relativeSourceMode.IndexOf('[');
if (!relativeSourceMode.EndsWith("]"))
{
throw new InvalidOperationException("Invalid RelativeSource binding syntax. Expected matching ']' for '['.");
}
var parentConfigParams = relativeSourceMode.Substring(parentConfigStart + 1).TrimEnd(']').Split(';');
if (parentConfigParams.Length > 2 || parentConfigParams.Length == 0)
{
throw new InvalidOperationException("Expected either 1 or 2 parameters for RelativeSource binding syntax");
}
else if (parentConfigParams.Length == 1)
{
if (int.TryParse(parentConfigParams[0], out int level))
{
relativeSource.AncestorType = null;
relativeSource.AncestorLevel = level + 1;
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
}
}
else
{
relativeSource.AncestorType = LookupAncestorType(parentConfigParams[0], context);
relativeSource.AncestorLevel = int.Parse(parentConfigParams[1]) + 1;
}
}
else
{
throw new InvalidOperationException($"Invalid RelativeSource binding syntax: {relativeSourceMode}");
}
}
else
{
result.Path = path;
}
return result;
}
private static Type LookupAncestorType(string ancestorTypeName, ITypeDescriptorContext context)
{
var parts = ancestorTypeName.Split(':');
if (parts.Length == 0 || parts.Length > 2)
{
throw new InvalidOperationException("Invalid type name");
}
if (parts.Length == 1)
{
return context.ResolveType(string.Empty, parts[0]);
}
else
{
return context.ResolveType(parts[0], parts[1]);
}
}
private static object GetDefaultAnchor(ITypeDescriptorContext context)
{

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs

@ -29,7 +29,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
ElementName = ElementName,
Mode = Mode,
RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent),
Path = Path,
Path = Path ?? string.Empty,
Priority = Priority,
};
}

91
src/Markup/Avalonia.Markup/ControlLocator.cs

@ -11,6 +11,21 @@ using Avalonia.VisualTree;
namespace Avalonia.Markup
{
/// <summary>
/// The type of tree via which to track a control.
/// </summary>
public enum TreeType
{
/// <summary>
/// The visual tree.
/// </summary>
Visual,
/// <summary>
/// The logical tree.
/// </summary>
Logical,
}
/// <summary>
/// Locates controls relative to other controls.
/// </summary>
@ -27,13 +42,13 @@ namespace Avalonia.Markup
{
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.AttachedToLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree += x)
x => relativeTo.AttachedToLogicalTree -= x)
.Select(x => ((IControl)x.Sender).FindNameScope())
.StartWith(relativeTo.FindNameScope());
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree += x)
x => relativeTo.DetachedFromLogicalTree -= x)
.Select(x => (INameScope)null);
return attached.Merge(detached).Select(nameScope =>
@ -68,37 +83,75 @@ namespace Avalonia.Markup
/// <param name="relativeTo">
/// The control relative from which the other control should be found.
/// </param>
/// <param name="ancestorType">The type of the ancestor to find.</param>
/// <param name="tree">The tree via which to track the control.</param>
/// <param name="ancestorLevel">
/// The level of ancestor control to look for. Use 0 for the first ancestor of the
/// requested type.
/// </param>
public static IObservable<IControl> Track(IControl relativeTo, Type ancestorType, int ancestorLevel)
/// <param name="ancestorType">The type of the ancestor to find.</param>
public static IObservable<IControl> Track(IControl relativeTo, TreeType tree, int ancestorLevel, Type ancestorType = null)
{
return TrackAttachmentToTree(relativeTo, tree).Select(isAttachedToTree =>
{
if (isAttachedToTree)
{
if (tree == TreeType.Visual)
{
return relativeTo.GetVisualAncestors()
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(ancestorLevel) as IControl;
}
else
{
return relativeTo.GetLogicalAncestors()
.Where(x => ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true)
.ElementAtOrDefault(ancestorLevel) as IControl;
}
}
else
{
return null;
}
});
}
private static IObservable<bool> TrackAttachmentToTree(IControl relativeTo, TreeType tree)
{
return tree == TreeType.Visual ? TrackAttachmentToVisualTree(relativeTo) : TrackAttachmentToLogicalTree(relativeTo);
}
private static IObservable<bool> TrackAttachmentToVisualTree(IControl relativeTo)
{
var attached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.AttachedToVisualTree += x,
x => relativeTo.DetachedFromVisualTree += x)
x => relativeTo.AttachedToVisualTree -= x)
.Select(x => true)
.StartWith(relativeTo.IsAttachedToVisualTree);
var detached = Observable.FromEventPattern<VisualTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromVisualTree += x,
x => relativeTo.DetachedFromVisualTree += x)
x => relativeTo.DetachedFromVisualTree -= x)
.Select(x => false);
return attached.Merge(detached).Select(isAttachedToVisualTree =>
{
if (isAttachedToVisualTree)
{
return relativeTo.GetVisualAncestors()
.Where(x => ancestorType.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()))
.ElementAtOrDefault(ancestorLevel) as IControl;
}
else
{
return null;
}
});
var attachmentStatus = attached.Merge(detached);
return attachmentStatus;
}
private static IObservable<bool> TrackAttachmentToLogicalTree(IControl relativeTo)
{
var attached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.AttachedToLogicalTree += x,
x => relativeTo.AttachedToLogicalTree -= x)
.Select(x => true)
.StartWith(relativeTo.IsAttachedToLogicalTree);
var detached = Observable.FromEventPattern<LogicalTreeAttachmentEventArgs>(
x => relativeTo.DetachedFromLogicalTree += x,
x => relativeTo.DetachedFromLogicalTree -= x)
.Select(x => false);
var attachmentStatus = attached.Merge(detached);
return attachmentStatus;
}
}
}

10
src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs

@ -51,15 +51,7 @@ namespace Avalonia.Markup.Data.Parsers
}
}
if (!r.End)
{
r.Take();
return result;
}
else
{
throw new ExpressionParseException(r.Position, "Expected ']'.");
}
throw new ExpressionParseException(r.Position, "Expected ']'.");
}
return null;

1
src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs

@ -54,7 +54,6 @@ namespace Avalonia.Direct2D1.Media
_finishedCallback = finishedCallback;
_directWriteFactory = directWriteFactory;
_imagingFactory = imagingFactory;
_swapChain = swapChain;
_renderTarget.BeginDraw();
}

22
tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs

@ -83,6 +83,28 @@ namespace Avalonia.Base.UnitTests.Collections
Assert.Equal(new[] { 6, 7, 8, 9, 10, 1, 2, 3, 4, 5 }, target);
}
[Fact]
public void MoveRange_Raises_Correct_CollectionChanged_Event()
{
var target = new AvaloniaList<int>(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 });
var raised = false;
target.CollectionChanged += (s, e) =>
{
Assert.Equal(NotifyCollectionChangedAction.Move, e.Action);
Assert.Equal(0, e.OldStartingIndex);
Assert.Equal(10, e.NewStartingIndex);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.OldItems);
Assert.Equal(new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, e.NewItems);
raised = true;
};
target.MoveRange(0, 9, 10);
Assert.True(raised);
Assert.Equal(new[] { 10, 1, 2, 3, 4, 5, 6, 7, 8, 9 }, target);
}
[Fact]
public void Adding_Item_Should_Raise_CollectionChanged()
{

63
tests/Avalonia.Controls.UnitTests/PanelTests.cs

@ -4,6 +4,7 @@
using System.Linq;
using Avalonia.Collections;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -18,8 +19,9 @@ namespace Avalonia.Controls.UnitTests
panel.Children.Add(child);
Assert.Equal(child.Parent, panel);
Assert.Equal(child.GetLogicalParent(), panel);
Assert.Same(child.Parent, panel);
Assert.Same(child.GetLogicalParent(), panel);
Assert.Same(child.GetVisualParent(), panel);
}
[Fact]
@ -45,6 +47,7 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(child.Parent);
Assert.Null(child.GetLogicalParent());
Assert.Null(child.GetVisualParent());
}
[Fact]
@ -60,8 +63,10 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(child1.Parent);
Assert.Null(child1.GetLogicalParent());
Assert.Null(child1.GetVisualParent());
Assert.Null(child2.Parent);
Assert.Null(child2.GetLogicalParent());
Assert.Null(child2.GetVisualParent());
}
[Fact]
@ -77,24 +82,32 @@ namespace Avalonia.Controls.UnitTests
Assert.Null(child1.Parent);
Assert.Null(child1.GetLogicalParent());
Assert.Null(child1.GetVisualParent());
Assert.Null(child2.Parent);
Assert.Null(child2.GetLogicalParent());
Assert.Null(child2.GetVisualParent());
}
[Fact]
public void Setting_Children_Should_Make_Controls_Appear_In_Panel_Children()
public void Replacing_Panel_Children_Should_Clear_And_Set_Control_Parent()
{
var panel = new Panel();
var child = new Control();
var child1 = new Control();
var child2 = new Control();
panel.Children = new Controls { child };
panel.Children.Add(child1);
panel.Children[0] = child2;
Assert.Equal(new[] { child }, panel.Children);
Assert.Equal(new[] { child }, panel.GetLogicalChildren());
Assert.Null(child1.Parent);
Assert.Null(child1.GetLogicalParent());
Assert.Null(child1.GetVisualParent());
Assert.Same(child2.Parent, panel);
Assert.Same(child2.GetLogicalParent(), panel);
Assert.Same(child2.GetVisualParent(), panel);
}
[Fact]
public void Child_Control_Should_Appear_In_Panel_Children()
public void Child_Control_Should_Appear_In_Panel_Logical_And_Visual_Children()
{
var panel = new Panel();
var child = new Control();
@ -103,10 +116,11 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new[] { child }, panel.Children);
Assert.Equal(new[] { child }, panel.GetLogicalChildren());
Assert.Equal(new[] { child }, panel.GetVisualChildren());
}
[Fact]
public void Removing_Child_Control_Should_Remove_From_Panel_Children()
public void Removing_Child_Control_Should_Remove_From_Panel_Logical_And_Visual_Children()
{
var panel = new Panel();
var child = new Control();
@ -115,7 +129,36 @@ namespace Avalonia.Controls.UnitTests
panel.Children.Remove(child);
Assert.Equal(new Control[0], panel.Children);
Assert.Equal(new ILogical[0], panel.GetLogicalChildren());
Assert.Empty(panel.GetLogicalChildren());
Assert.Empty(panel.GetVisualChildren());
}
[Fact]
public void Moving_Panel_Children_Should_Reoder_Logical_And_Visual_Children()
{
var panel = new Panel();
var child1 = new Control();
var child2 = new Control();
panel.Children.Add(child1);
panel.Children.Add(child2);
panel.Children.Move(1, 0);
Assert.Equal(new[] { child2, child1 }, panel.GetLogicalChildren());
Assert.Equal(new[] { child2, child1 }, panel.GetVisualChildren());
}
[Fact]
public void Setting_Children_Should_Make_Controls_Appear_In_Logical_And_Visual_Children()
{
var panel = new Panel();
var child = new Control();
panel.Children = new Controls { child };
Assert.Equal(new[] { child }, panel.Children);
Assert.Equal(new[] { child }, panel.GetLogicalChildren());
Assert.Equal(new[] { child }, panel.GetVisualChildren());
}
}
}

23
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs

@ -13,6 +13,7 @@ using Moq;
using Xunit;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Avalonia.UnitTests;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
@ -337,6 +338,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
Assert.Equal("foo", target.Content);
}
[Fact]
public void Binding_With_Null_Path_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Name='textBlock' Text='{Binding}'/>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
window.DataContext = "foo";
window.ApplyTemplate();
Assert.Equal("foo", textBlock.Text);
}
}
private class TwoWayBindingTest : Control
{
public static readonly StyledProperty<string> TwoWayProperty =

32
tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs

@ -11,6 +11,9 @@ using Avalonia.Markup.Xaml.Data;
using Avalonia.Styling;
using Xunit;
using System.Reactive.Disposables;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using System.Linq;
namespace Avalonia.Markup.Xaml.UnitTests.Data
{
@ -56,6 +59,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data
BindingPriority.TemplatedParent));
}
[Fact]
public void TemplateBinding_With_Null_Path_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Button Name='button'>
<Button.Template>
<ControlTemplate>
<TextBlock Text='{TemplateBinding}'/>
</ControlTemplate>
</Button.Template>
</Button>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
button.ApplyTemplate();
var textBlock = (TextBlock)button.GetVisualChildren().Single();
Assert.Equal("Avalonia.Controls.Button", textBlock.Text);
}
}
private Mock<IControl> CreateTarget(
ITemplatedControl templatedParent = null,
string text = null)

174
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

@ -3,6 +3,7 @@
using Avalonia.Controls;
using Avalonia.UnitTests;
using System;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.Xaml
@ -77,6 +78,77 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Binding_To_First_Ancestor_Without_AncestorType_Throws_Exception()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<ContentControl Name='contentControl'>
<Button Name='button' Content='{Binding Name, RelativeSource={RelativeSource AncestorLevel=1}}'/>
</ContentControl>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
Assert.Throws<InvalidOperationException>( () => loader.Load(xaml));
}
}
[Fact]
public void Binding_To_First_Ancestor_With_Shorthand_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent.Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border2", button.Content);
}
}
[Fact]
public void Binding_To_First_Ancestor_With_Shorthand_Uses_LogicalTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border'>
<ContentControl Name='contentControl'>
<Button Name='button' Content='{Binding $parent.Name}'/>
</ContentControl>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var contentControl = window.FindControl<ContentControl>("contentControl");
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("contentControl", button.Content);
}
}
[Fact]
public void Binding_To_Second_Ancestor_Works()
{
@ -102,6 +174,108 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Binding_To_Second_Ancestor_With_Shorthand_Uses_LogicalTree()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<ContentControl Name='contentControl1'>
<ContentControl Name='contentControl2'>
<Button Name='button' Content='{Binding $parent[1].Name}'/>
</ContentControl>
</ContentControl>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var contentControl1 = window.FindControl<ContentControl>("contentControl1");
var contentControl2 = window.FindControl<ContentControl>("contentControl2");
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("contentControl1", button.Content);
}
}
[Fact]
public void Binding_To_Ancestor_Of_Type_With_Shorthand_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent[Border].Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border2", button.Content);
}
}
[Fact]
public void Binding_To_Second_Ancestor_With_Shorthand_And_Type_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent[Border; 1].Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border1", button.Content);
}
}
[Fact]
public void Binding_To_Second_Ancestor_With_Shorthand_Works()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
<Border Name='border1'>
<Border Name='border2'>
<Button Name='button' Content='{Binding $parent[1].Name}'/>
</Border>
</Border>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var button = window.FindControl<Button>("button");
window.ApplyTemplate();
Assert.Equal("border1", button.Content);
}
}
[Fact]
public void Binding_To_Ancestor_With_Namespace_Works()
{

Loading…
Cancel
Save