Browse Source

Notify DataContextChanged down tree.

When a data context changes, it should be notified to all descendant controls who inherit the data context. This allows the `SelectingItemsControl` to defer setting the selected item until the `DataContext` has changed for the whole tree.

Renamed `OnDataContextChanging`/`OnDataContextChanged` to `OnDataContextBeginUpdate`/`OnDataContextEndUpdate` because `DataContextChanged` is now called from the property changed notification.

Fixes #507.
pull/1238/head
Steven Kirk 8 years ago
parent
commit
181dd53e58
  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

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();
}
}
}
}

Loading…
Cancel
Save