Browse Source

Merge pull request #1238 from AvaloniaUI/fixes/507-selectedindex-binding

Notify DataContextChanged down tree.
pull/1070/merge
Jeremy Koritzinsky 8 years ago
committed by GitHub
parent
commit
8be9cc6a2c
  1. 61
      src/Avalonia.Controls/Control.cs
  2. 8
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  3. 10
      src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
  4. 133
      tests/Avalonia.Controls.UnitTests/ControlTests.cs
  5. 59
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs

61
src/Avalonia.Controls/Control.cs

@ -101,6 +101,7 @@ namespace Avalonia.Controls
private Styles _styles;
private bool _styled;
private Subject<IStyleable> _styleDetach = new Subject<IStyleable>();
private bool _dataContextUpdating;
/// <summary>
/// Initializes static members of the <see cref="Control"/> class.
@ -111,6 +112,7 @@ namespace Avalonia.Controls
PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled");
PseudoClass(IsFocusedProperty, ":focus");
PseudoClass(IsPointerOverProperty, ":pointerover");
DataContextProperty.Changed.AddClassHandler<Control>(x => x.OnDataContextChangedCore);
}
/// <summary>
@ -681,18 +683,26 @@ namespace Avalonia.Controls
}
/// <summary>
/// Called before the <see cref="DataContext"/> property changes.
/// Called when the <see cref="DataContext"/> property changes.
/// </summary>
protected virtual void OnDataContextChanging()
/// <param name="e">The event args.</param>
protected virtual void OnDataContextChanged(EventArgs e)
{
DataContextChanged?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Called after the <see cref="DataContext"/> property changes.
/// Called when the <see cref="DataContext"/> begins updating.
/// </summary>
protected virtual void OnDataContextChanged()
protected virtual void OnDataContextBeginUpdate()
{
}
/// <summary>
/// Called when the <see cref="DataContext"/> finishes updating.
/// </summary>
protected virtual void OnDataContextEndUpdate()
{
DataContextChanged?.Invoke(this, EventArgs.Empty);
}
/// <inheritdoc/>
@ -745,24 +755,38 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Called when the <see cref="DataContext"/> property begins and ends being notified.
/// </summary>
/// <param name="o">The object on which the DataContext is changing.</param>
/// <param name="notifying">Whether the notifcation is beginning or ending.</param>
private static void DataContextNotifying(IAvaloniaObject o, bool notifying)
{
var control = o as Control;
if (o is Control control)
{
DataContextNotifying(control, notifying);
}
}
if (control != null)
private static void DataContextNotifying(Control control, bool notifying)
{
if (notifying)
{
if (notifying)
if (!control._dataContextUpdating)
{
control.OnDataContextChanging();
control._dataContextUpdating = true;
control.OnDataContextBeginUpdate();
foreach (var child in control.LogicalChildren)
{
if (child is Control c && !c.IsSet(DataContextProperty))
{
DataContextNotifying(c, notifying);
}
}
}
else
}
else
{
if (control._dataContextUpdating)
{
control.OnDataContextChanged();
control.OnDataContextEndUpdate();
control._dataContextUpdating = false;
}
}
}
@ -881,6 +905,11 @@ namespace Avalonia.Controls
}
}
private void OnDataContextChangedCore(AvaloniaPropertyChangedEventArgs e)
{
OnDataContextChanged(EventArgs.Empty);
}
private void LogicalChildrenCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)

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

@ -414,16 +414,16 @@ namespace Avalonia.Controls.Primitives
}
/// <inheritdoc/>
protected override void OnDataContextChanging()
protected override void OnDataContextBeginUpdate()
{
base.OnDataContextChanging();
base.OnDataContextBeginUpdate();
++_updateCount;
}
/// <inheritdoc/>
protected override void OnDataContextChanged()
protected override void OnDataContextEndUpdate()
{
base.OnDataContextChanged();
base.OnDataContextEndUpdate();
if (--_updateCount == 0)
{

10
src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs

@ -50,6 +50,16 @@ namespace Avalonia.LogicalTree
}
}
public static IEnumerable<ILogical> GetSelfAndLogicalDescendants(this ILogical logical)
{
yield return logical;
foreach (var descendent in logical.GetLogicalDescendants())
{
yield return descendent;
}
}
public static ILogical GetLogicalParent(this ILogical logical)
{
return logical.LogicalParent;

133
tests/Avalonia.Controls.UnitTests/ControlTests.cs

@ -8,6 +8,7 @@ using Moq;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Xunit;
using Avalonia.LogicalTree;
namespace Avalonia.Controls.UnitTests
{
@ -328,9 +329,139 @@ namespace Avalonia.Controls.UnitTests
Assert.True(target.IsInitialized);
}
private class TestControl : Control
[Fact]
public void DataContextChanged_Should_Be_Called()
{
var root = new TestStackPanel
{
Name = "root",
Children =
{
new TestControl
{
Name = "a1",
Child = new TestControl
{
Name = "b1",
}
},
new TestControl
{
Name = "a2",
DataContext = "foo",
},
}
};
var called = new List<string>();
void Record(object sender, EventArgs e) => called.Add(((Control)sender).Name);
root.DataContextChanged += Record;
foreach (TestControl c in root.GetLogicalDescendants())
{
c.DataContextChanged += Record;
}
root.DataContext = "foo";
Assert.Equal(new[] { "root", "a1", "b1", }, called);
}
[Fact]
public void DataContext_Notifications_Should_Be_Called_In_Correct_Order()
{
var root = new TestStackPanel
{
Name = "root",
Children =
{
new TestControl
{
Name = "a1",
Child = new TestControl
{
Name = "b1",
}
},
new TestControl
{
Name = "a2",
DataContext = "foo",
},
}
};
var called = new List<string>();
foreach (IDataContextEvents c in root.GetSelfAndLogicalDescendants())
{
c.DataContextBeginUpdate += (s, e) => called.Add("begin " + ((Control)s).Name);
c.DataContextChanged += (s, e) => called.Add("changed " + ((Control)s).Name);
c.DataContextEndUpdate += (s, e) => called.Add("end " + ((Control)s).Name);
}
root.DataContext = "foo";
Assert.Equal(
new[]
{
"begin root",
"begin a1",
"begin b1",
"changed root",
"changed a1",
"changed b1",
"end b1",
"end a1",
"end root",
},
called);
}
private interface IDataContextEvents
{
event EventHandler DataContextBeginUpdate;
event EventHandler DataContextChanged;
event EventHandler DataContextEndUpdate;
}
private class TestControl : Decorator, IDataContextEvents
{
public event EventHandler DataContextBeginUpdate;
public event EventHandler DataContextEndUpdate;
public new IAvaloniaObject InheritanceParent => base.InheritanceParent;
protected override void OnDataContextBeginUpdate()
{
DataContextBeginUpdate?.Invoke(this, EventArgs.Empty);
base.OnDataContextBeginUpdate();
}
protected override void OnDataContextEndUpdate()
{
DataContextEndUpdate?.Invoke(this, EventArgs.Empty);
base.OnDataContextEndUpdate();
}
}
private class TestStackPanel : StackPanel, IDataContextEvents
{
public event EventHandler DataContextBeginUpdate;
public event EventHandler DataContextEndUpdate;
protected override void OnDataContextBeginUpdate()
{
DataContextBeginUpdate?.Invoke(this, EventArgs.Empty);
base.OnDataContextBeginUpdate();
}
protected override void OnDataContextEndUpdate()
{
DataContextEndUpdate?.Invoke(this, EventArgs.Empty);
base.OnDataContextEndUpdate();
}
}
}
}

59
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs

@ -1,7 +1,10 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System.Collections.Generic;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.UnitTests;
using Xunit;
@ -67,5 +70,61 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
Assert.True(called);
}
}
[Fact]
public void Can_Bind_Between_TabStrip_And_Carousel()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'>
<DockPanel>
<TabStrip Name='strip' DockPanel.Dock='Top' Items='{Binding Items}' SelectedIndex='0'>
<TabStrip.DataTemplates>
<DataTemplate>
<TextBlock Text='{Binding Header}'/>
</DataTemplate>
</TabStrip.DataTemplates>
</TabStrip>
<Carousel Name='carousel' Items='{Binding Items}' SelectedIndex='{Binding #strip.SelectedIndex}'>
<Carousel.DataTemplates>
<DataTemplate>
<TextBlock Text='{Binding Detail}'/>
</DataTemplate>
</Carousel.DataTemplates>
</Carousel>
</DockPanel>
</Window>";
var loader = new AvaloniaXamlLoader();
var window = (Window)loader.Load(xaml);
var strip = window.FindControl<TabStrip>("strip");
var carousel = window.FindControl<Carousel>("carousel");
window.DataContext = new ItemsViewModel
{
Items = new[]
{
new ItemViewModel { Header = "Item1", Detail = "Detail1" },
new ItemViewModel { Header = "Item2", Detail = "Detail2" },
}
};
window.Show();
Assert.Equal(0, strip.SelectedIndex);
Assert.Equal(0, carousel.SelectedIndex);
}
}
private class ItemsViewModel
{
public IList<ItemViewModel> Items { get; set; }
}
private class ItemViewModel
{
public string Header { get; set; }
public string Detail { get; set; }
}
}
}

Loading…
Cancel
Save