diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 89c4f9279a..a7aafbf6e8 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -101,6 +101,7 @@ namespace Avalonia.Controls private Styles _styles; private bool _styled; private Subject _styleDetach = new Subject(); + private bool _dataContextUpdating; /// /// Initializes static members of the class. @@ -111,6 +112,7 @@ namespace Avalonia.Controls PseudoClass(IsEnabledCoreProperty, x => !x, ":disabled"); PseudoClass(IsFocusedProperty, ":focus"); PseudoClass(IsPointerOverProperty, ":pointerover"); + DataContextProperty.Changed.AddClassHandler(x => x.OnDataContextChangedCore); } /// @@ -681,18 +683,26 @@ namespace Avalonia.Controls } /// - /// Called before the property changes. + /// Called when the property changes. /// - protected virtual void OnDataContextChanging() + /// The event args. + protected virtual void OnDataContextChanged(EventArgs e) { + DataContextChanged?.Invoke(this, EventArgs.Empty); } /// - /// Called after the property changes. + /// Called when the begins updating. /// - protected virtual void OnDataContextChanged() + protected virtual void OnDataContextBeginUpdate() + { + } + + /// + /// Called when the finishes updating. + /// + protected virtual void OnDataContextEndUpdate() { - DataContextChanged?.Invoke(this, EventArgs.Empty); } /// @@ -745,24 +755,38 @@ namespace Avalonia.Controls } } - /// - /// Called when the property begins and ends being notified. - /// - /// The object on which the DataContext is changing. - /// Whether the notifcation is beginning or ending. 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) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 27f3407fd9..ab09a4701d 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -414,16 +414,16 @@ namespace Avalonia.Controls.Primitives } /// - protected override void OnDataContextChanging() + protected override void OnDataContextBeginUpdate() { - base.OnDataContextChanging(); + base.OnDataContextBeginUpdate(); ++_updateCount; } /// - protected override void OnDataContextChanged() + protected override void OnDataContextEndUpdate() { - base.OnDataContextChanged(); + base.OnDataContextEndUpdate(); if (--_updateCount == 0) { diff --git a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs index a72a558258..276ea6c060 100644 --- a/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs +++ b/src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs @@ -50,6 +50,16 @@ namespace Avalonia.LogicalTree } } + public static IEnumerable 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; diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests.cs b/tests/Avalonia.Controls.UnitTests/ControlTests.cs index b7e2cbeca9..3506a1606b 100644 --- a/tests/Avalonia.Controls.UnitTests/ControlTests.cs +++ b/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(); + 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(); + + 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(); + } } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs index a508c21747..bd9d99ff23 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ControlBindingTests.cs +++ b/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 = @" + + + + + + + + + + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var strip = window.FindControl("strip"); + var carousel = window.FindControl("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 Items { get; set; } + } + + private class ItemViewModel + { + public string Header { get; set; } + public string Detail { get; set; } + } } }