diff --git a/.gitignore b/.gitignore index b5a46e16f4..7d672c7755 100644 --- a/.gitignore +++ b/.gitignore @@ -117,6 +117,7 @@ ClientBin/ *.[Pp]ublish.xml *.pfx *.publishsettings +Events_Avalonia.cs # RIA/Silverlight projects Generated_Code/ diff --git a/.ncrunch/Sandbox.v3.ncrunchproject b/.ncrunch/Sandbox.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/Sandbox.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.sln b/Avalonia.sln index ddcd61408d..34ad19b41d 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -224,7 +224,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Markup.Xaml.Loader EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events", "src\Avalonia.ReactiveUI.Events\Avalonia.ReactiveUI.Events.csproj", "{28F18757-C3E6-4BBE-A37D-11BA2AB9177C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.ReactiveUI.Events.UnitTests", "tests\Avalonia.ReactiveUI.Events.UnitTests\Avalonia.ReactiveUI.Events.UnitTests.csproj", "{780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sandbox", "samples\Sandbox\Sandbox.csproj", "{11BE52AF-E2DD-4CF0-B19A-05285ACAF571}" EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution @@ -2040,30 +2040,30 @@ Global {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhone.Build.0 = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {28F18757-C3E6-4BBE-A37D-11BA2AB9177C}.Release|iPhoneSimulator.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhone.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|Any CPU.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhone.Build.0 = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Ad-Hoc|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.AppStore|iPhoneSimulator.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhone.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|Any CPU.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhone.Build.0 = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2122,7 +2122,7 @@ Global {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} - {780AC0FE-8DD2-419F-A1DC-AC7E3EB393F7} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} + {11BE52AF-E2DD-4CF0-B19A-05285ACAF571} = {9B9E3891-2366-4253-A952-D08BCEB71098} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 24398accbb..7e2bbc13bc 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -241,7 +241,6 @@ partial class Build : NukeBuild RunCoreTest("Avalonia.Visuals.UnitTests"); RunCoreTest("Avalonia.Skia.UnitTests"); RunCoreTest("Avalonia.ReactiveUI.UnitTests"); - RunCoreTest("Avalonia.ReactiveUI.Events.UnitTests"); }); Target RunRenderTests => _ => _ diff --git a/samples/Sandbox/App.axaml b/samples/Sandbox/App.axaml new file mode 100644 index 0000000000..699781eb94 --- /dev/null +++ b/samples/Sandbox/App.axaml @@ -0,0 +1,8 @@ + + + + + diff --git a/samples/Sandbox/App.axaml.cs b/samples/Sandbox/App.axaml.cs new file mode 100644 index 0000000000..7eb8345784 --- /dev/null +++ b/samples/Sandbox/App.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; + +namespace Sandbox +{ + public class App : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime) + { + desktopLifetime.MainWindow = new MainWindow(); + } + } + } +} diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml new file mode 100644 index 0000000000..6929f192c7 --- /dev/null +++ b/samples/Sandbox/MainWindow.axaml @@ -0,0 +1,4 @@ + + diff --git a/samples/Sandbox/MainWindow.axaml.cs b/samples/Sandbox/MainWindow.axaml.cs new file mode 100644 index 0000000000..b7222e043d --- /dev/null +++ b/samples/Sandbox/MainWindow.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Sandbox +{ + public class MainWindow : Window + { + public MainWindow() + { + this.InitializeComponent(); + this.AttachDevTools(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/Sandbox/Program.cs b/samples/Sandbox/Program.cs new file mode 100644 index 0000000000..4d7eda8d9f --- /dev/null +++ b/samples/Sandbox/Program.cs @@ -0,0 +1,17 @@ +using Avalonia; +using Avalonia.ReactiveUI; + +namespace Sandbox +{ + public class Program + { + static void Main(string[] args) + { + AppBuilder.Configure() + .UsePlatformDetect() + .UseReactiveUI() + .LogToDebug() + .StartWithClassicDesktopLifetime(args); + } + } +} diff --git a/samples/Sandbox/Sandbox.csproj b/samples/Sandbox/Sandbox.csproj new file mode 100644 index 0000000000..1a0a8a7ce5 --- /dev/null +++ b/samples/Sandbox/Sandbox.csproj @@ -0,0 +1,18 @@ + + + + WinExe + netcoreapp3.1 + true + + + + + + + + + + + + diff --git a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs index 8e82bf1a38..1e72a07760 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs @@ -49,8 +49,12 @@ namespace Avalonia.Controls { if(_binding is Avalonia.Data.Binding binding) { - // Force the TwoWay binding mode if there is a Path present. TwoWay binding requires a Path. - if (!String.IsNullOrEmpty(binding.Path)) + if (binding.Mode == BindingMode.OneWayToSource) + { + throw new InvalidOperationException("DataGridColumn doesn't support BindingMode.OneWayToSource. Use BindingMode.TwoWay instead."); + } + + if (!String.IsNullOrEmpty(binding.Path) && binding.Mode == BindingMode.Default) { binding.Mode = BindingMode.TwoWay; } diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index fd27cb340a..3b5d57a7b8 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -242,12 +242,7 @@ namespace Avalonia.Controls.Selection { using var update = BatchUpdate(); var o = update.Operation; - var range = CoerceRange(start, end); - - if (range.Begin == -1) - { - return; - } + var range = new IndexRange(Math.Max(0, start), end); if (RangesEnabled) { diff --git a/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj index b95c8946e2..75eeb92f42 100644 --- a/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj +++ b/src/Avalonia.ReactiveUI.Events/Avalonia.ReactiveUI.Events.csproj @@ -1,7 +1,7 @@  netstandard2.0 - Avalonia.ReactiveUI + Avalonia.ReactiveUI.Events diff --git a/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs b/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs index 79e029ccd2..9cc94ffac3 100644 --- a/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs +++ b/src/Avalonia.Visuals/Rendering/SleepLoopRenderTimer.cs @@ -45,14 +45,13 @@ namespace Avalonia.Rendering void LoopProc() { - var now = _st.Elapsed; - var lastTick = now; - + var lastTick = _st.Elapsed; while (true) { + var now = _st.Elapsed; var timeTillNextTick = lastTick + _timeBetweenTicks - now; if (timeTillNextTick.TotalMilliseconds > 1) Thread.Sleep(timeTillNextTick); - + lastTick = now; lock (_lock) { if (_count == 0) @@ -63,7 +62,7 @@ namespace Avalonia.Rendering } _tick?.Invoke(now); - now = _st.Elapsed; + } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 192d6e0286..514d3b5475 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1813,6 +1813,88 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, raised); } + [Fact] + public void Handles_Removing_Last_Item_In_Two_Controls_With_Bound_SelectedIndex() + { + var items = new ObservableCollection { "foo" }; + + // Simulates problem with TabStrip and Carousel with bound SelectedIndex. + var tabStrip = new TestSelector + { + Items = items, + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var carousel = new TestSelector + { + Items = items, + [!Carousel.SelectedIndexProperty] = tabStrip[!TabStrip.SelectedIndexProperty], + }; + + var tabStripRaised = 0; + var carouselRaised = 0; + + tabStrip.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++tabStripRaised; + }; + + carousel.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++carouselRaised; + }; + + items.RemoveAt(0); + + Assert.Equal(1, tabStripRaised); + Assert.Equal(1, carouselRaised); + } + + [Fact] + public void Handles_Removing_Last_Item_In_Controls_With_Bound_SelectedItem() + { + var items = new ObservableCollection { "foo" }; + + // Simulates problem with TabStrip and Carousel with bound SelectedItem. + var tabStrip = new TestSelector + { + Items = items, + SelectionMode = SelectionMode.AlwaysSelected, + }; + + var carousel = new TestSelector + { + Items = items, + [!Carousel.SelectedItemProperty] = tabStrip[!TabStrip.SelectedItemProperty], + }; + + var tabStripRaised = 0; + var carouselRaised = 0; + + tabStrip.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++tabStripRaised; + }; + + carousel.SelectionChanged += (s, e) => + { + Assert.Equal(new[] { "foo" }, e.RemovedItems); + Assert.Empty(e.AddedItems); + ++carouselRaised; + }; + + items.RemoveAt(0); + + Assert.Equal(1, tabStripRaised); + Assert.Equal(1, carouselRaised); + } + private static void Prepare(SelectingItemsControl target) { var root = new TestRoot diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj b/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj deleted file mode 100644 index 19a6fd138e..0000000000 --- a/tests/Avalonia.ReactiveUI.Events.UnitTests/Avalonia.ReactiveUI.Events.UnitTests.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - netcoreapp3.1 - - - - - - - - - - - - diff --git a/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs b/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs deleted file mode 100644 index 1092c98246..0000000000 --- a/tests/Avalonia.ReactiveUI.Events.UnitTests/BasicControlEventsTest.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Reactive.Linq; -using Avalonia.Controls; -using Avalonia.UnitTests; -using Xunit; - -namespace Avalonia.ReactiveUI.Events.UnitTests -{ - public class BasicControlEventsTest - { - public class EventsControl : UserControl - { - public bool IsAttached { get; private set; } - - public EventsControl() - { - var attached = this - .Events() - .AttachedToVisualTree - .Select(args => true); - - this.Events() - .DetachedFromVisualTree - .Select(args => false) - .Merge(attached) - .Subscribe(marker => IsAttached = marker); - } - } - - [Fact] - public void Should_Generate_Events_Wrappers() - { - var root = new TestRoot(); - var control = new EventsControl(); - Assert.False(control.IsAttached); - - root.Child = control; - Assert.True(control.IsAttached); - - root.Child = null; - Assert.False(control.IsAttached); - } - } -}