diff --git a/.gitignore b/.gitignore index e67d986a8a..a9a8fd36b4 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,5 @@ nuget Avalonia.XBuild.sln project.lock.json .idea/* +**/obj-Skia/* +**/obj-Direct2D1/* diff --git a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject index a8c3abe8f2..04ab17c4e1 100644 --- a/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject +++ b/.ncrunch/Avalonia.Direct2D1.RenderTests.v3.ncrunchproject @@ -1,7 +1,6 @@  - 1000 - True + 3000 True \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index ba3680d0b9..76d1ae3e1c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -os: Previous Visual Studio 2017 +os: Visual Studio 2017 platform: - Any CPU skip_branch_with_pr: true diff --git a/build.cake b/build.cake index eb5918832e..61fda13695 100644 --- a/build.cake +++ b/build.cake @@ -93,7 +93,7 @@ Task("Clean") CleanDirectory(parameters.NugetRoot); CleanDirectory(parameters.ZipRoot); CleanDirectory(parameters.BinRoot); - CleanDirectory(parameters.TestsRoot); + CleanDirectory(parameters.DesignerTestsRoot); }); Task("Restore-NuGet-Packages") @@ -124,14 +124,9 @@ Task("Restore-NuGet-Packages") void DotNetCoreBuild() { - DotNetCoreRestore("samples\\ControlCatalog.NetCore"); - DotNetBuild("samples\\ControlCatalog.NetCore"); + DotNetCoreBuild("samples\\ControlCatalog.NetCore"); } -Task("DotNetCoreBuild") - .IsDependentOn("Clean") - .Does(() => DotNetCoreBuild()); - Task("Build") .IsDependentOn("Restore-NuGet-Packages") .Does(() => @@ -140,11 +135,11 @@ Task("Build") { MSBuild(parameters.MSBuildSolution, settings => { settings.SetConfiguration(parameters.Configuration); + settings.SetVerbosity(Verbosity.Minimal); settings.WithProperty("Platform", "\"" + parameters.Platform + "\""); settings.WithProperty("UseRoslynPathHack", "true"); - settings.SetVerbosity(Verbosity.Minimal); - settings.WithProperty("Windows", "True"); settings.UseToolVersion(MSBuildToolVersion.VS2017); + settings.WithProperty("Windows", "True"); settings.SetNodeReuse(false); }); } @@ -160,10 +155,9 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) if(!project.EndsWith(".csproj")) project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj"); Information("Running tests from " + project); - DotNetCoreRestore(project); var frameworks = new List(){"netcoreapp2.0"}; if(parameters.IsRunningOnWindows) - frameworks.Add("net461"); + frameworks.Add("net47"); foreach(var fw in frameworks) { if(!fw.StartsWith("netcoreapp") && coreOnly) @@ -178,8 +172,11 @@ void RunCoreTest(string project, Parameters parameters, bool coreOnly = false) } } -Task("Run-Net-Core-Unit-Tests") - .IsDependentOn("Clean") +Task("Run-Unit-Tests") + .IsDependentOn("Build") + .IsDependentOn("Run-Designer-Unit-Tests") + .IsDependentOn("Run-Render-Tests") + .WithCriteria(() => !parameters.SkipTests) .Does(() => { RunCoreTest("./tests/Avalonia.Base.UnitTests", parameters, false); RunCoreTest("./tests/Avalonia.Controls.UnitTests", parameters, false); @@ -190,27 +187,25 @@ Task("Run-Net-Core-Unit-Tests") RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests", parameters, false); RunCoreTest("./tests/Avalonia.Styling.UnitTests", parameters, false); RunCoreTest("./tests/Avalonia.Visuals.UnitTests", parameters, false); - if(parameters.IsRunningOnWindows) - RunCoreTest("./tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.csproj", parameters, true); + if (parameters.IsRunningOnWindows) + { + RunCoreTest("./tests/Avalonia.Direct2D1.UnitTests", parameters, true); + } }); -Task("Run-Unit-Tests") - .IsDependentOn("Run-Net-Core-Unit-Tests") +Task("Run-Render-Tests") .IsDependentOn("Build") - //.IsDependentOn("Run-Leak-Tests") - .WithCriteria(() => !parameters.SkipTests) + .WithCriteria(() => !parameters.SkipTests && parameters.IsRunningOnWindows) + .Does(() => { + RunCoreTest("./tests/Avalonia.RenderTests/Avalonia.Skia.RenderTests.csproj", parameters, true); + RunCoreTest("./tests/Avalonia.RenderTests/Avalonia.Direct2D1.RenderTests.csproj", parameters, true); + }); + +Task("Run-Designer-Unit-Tests") + .IsDependentOn("Build") + .WithCriteria(() => !parameters.SkipTests && parameters.IsRunningOnWindows) .Does(() => { - if(!parameters.IsRunningOnWindows) - return; - - var unitTests = GetDirectories("./tests/Avalonia.*.UnitTests") - .Select(dir => System.IO.Path.GetFileName(dir.FullPath)) - .Where( name => !name.Contains("Skia")) // Run in the Run-Net-Core-Unit-Tests target - .Where(name => parameters.IsRunningOnWindows ? true : !name.Contains("Direct2D")) - .Select(name => MakeAbsolute(File("./tests/" + name + "/bin/" + parameters.DirSuffix + "/" + name + ".dll"))) - .ToList(); - var toolPath = (parameters.IsPlatformAnyCPU || parameters.IsPlatformX86) ? Context.Tools.Resolve("xunit.console.x86.exe") : Context.Tools.Resolve("xunit.console.exe"); @@ -219,27 +214,10 @@ Task("Run-Unit-Tests") { ToolPath = toolPath, Parallelism = ParallelismOption.None, - ShadowCopy = false + ShadowCopy = false, }; - xUnitSettings.NoAppDomain = !parameters.IsRunningOnWindows; - - foreach(var test in unitTests.Where(testFile => FileExists(testFile))) - { - CopyDirectory(test.GetDirectory(), parameters.TestsRoot); - } - - var testsInDirectoryToRun = new List(); - if(parameters.IsRunningOnWindows) - { - testsInDirectoryToRun.AddRange(GetFiles("./artifacts/tests/*Tests.dll")); - } - else - { - testsInDirectoryToRun.AddRange(GetFiles("./artifacts/tests/*.UnitTests.dll")); - } - - XUnit2(testsInDirectoryToRun, xUnitSettings); + XUnit2("./artifacts/designer-tests/Avalonia.DesignerSupport.Tests.dll", xUnitSettings); }); Task("Copy-Files") @@ -427,7 +405,7 @@ Task("Default").Does(() => if(parameters.IsRunningOnWindows) RunTarget("Package"); else - RunTarget("Run-Net-Core-Unit-Tests"); + RunTarget("Run-Unit-Tests"); }); Task("AppVeyor") .IsDependentOn("Zip-Files") @@ -435,7 +413,7 @@ Task("AppVeyor") .IsDependentOn("Publish-NuGet"); Task("Travis") - .IsDependentOn("Run-Net-Core-Unit-Tests"); + .IsDependentOn("Run-Unit-Tests"); /////////////////////////////////////////////////////////////////////////////// // EXECUTE diff --git a/parameters.cake b/parameters.cake index 7406618763..c727b3107f 100644 --- a/parameters.cake +++ b/parameters.cake @@ -30,7 +30,7 @@ public class Parameters public DirectoryPath NugetRoot { get; private set; } public DirectoryPath ZipRoot { get; private set; } public DirectoryPath BinRoot { get; private set; } - public DirectoryPath TestsRoot { get; private set; } + public DirectoryPath DesignerTestsRoot { get; private set; } public string DirSuffix { get; private set; } public string DirSuffixIOS { get; private set; } public DirectoryPathCollection BuildDirs { get; private set; } @@ -106,7 +106,7 @@ public class Parameters NugetRoot = ArtifactsDir.Combine("nuget"); ZipRoot = ArtifactsDir.Combine("zip"); BinRoot = ArtifactsDir.Combine("bin"); - TestsRoot = ArtifactsDir.Combine("tests"); + DesignerTestsRoot = ArtifactsDir.Combine("designer-tests"); BuildDirs = context.GetDirectories("**/bin") + context.GetDirectories("**/obj"); diff --git a/samples/BindingTest/App.config b/samples/BindingTest/App.config index 373b3a13f8..538be69997 100644 --- a/samples/BindingTest/App.config +++ b/samples/BindingTest/App.config @@ -1,7 +1,7 @@ - + diff --git a/samples/BindingTest/BindingTest.csproj b/samples/BindingTest/BindingTest.csproj index 9f3fed5522..a17fe0eed1 100644 --- a/samples/BindingTest/BindingTest.csproj +++ b/samples/BindingTest/BindingTest.csproj @@ -9,7 +9,7 @@ Properties BindingTest BindingTest - v4.6.1 + v4.7 512 true diff --git a/samples/ControlCatalog.Desktop/App.config b/samples/ControlCatalog.Desktop/App.config index 68403e421f..cd4593817b 100644 --- a/samples/ControlCatalog.Desktop/App.config +++ b/samples/ControlCatalog.Desktop/App.config @@ -1,7 +1,7 @@ - + diff --git a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj index cfa2f89b0e..8a5959e361 100644 --- a/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj +++ b/samples/ControlCatalog.Desktop/ControlCatalog.Desktop.csproj @@ -9,7 +9,7 @@ Properties ControlCatalog.Desktop ControlCatalog.Desktop - v4.6.1 + v4.7 512 true diff --git a/samples/RenderTest/App.config b/samples/RenderTest/App.config index 68403e421f..cd4593817b 100644 --- a/samples/RenderTest/App.config +++ b/samples/RenderTest/App.config @@ -1,7 +1,7 @@ - + diff --git a/samples/RenderTest/RenderTest.csproj b/samples/RenderTest/RenderTest.csproj index 4a471a831a..b33d5d3c70 100644 --- a/samples/RenderTest/RenderTest.csproj +++ b/samples/RenderTest/RenderTest.csproj @@ -9,7 +9,7 @@ Properties RenderTest RenderTest - v4.6.1 + v4.7 512 true diff --git a/samples/VirtualizationTest/App.config b/samples/VirtualizationTest/App.config index 68403e421f..cd4593817b 100644 --- a/samples/VirtualizationTest/App.config +++ b/samples/VirtualizationTest/App.config @@ -1,7 +1,7 @@ - + diff --git a/samples/VirtualizationTest/VirtualizationTest.csproj b/samples/VirtualizationTest/VirtualizationTest.csproj index 147355aed1..0d498d827f 100644 --- a/samples/VirtualizationTest/VirtualizationTest.csproj +++ b/samples/VirtualizationTest/VirtualizationTest.csproj @@ -9,7 +9,7 @@ Properties VirtualizationTest VirtualizationTest - v4.6.1 + v4.7 512 true diff --git a/scripts/ReplaceNugetCache.ps1 b/scripts/ReplaceNugetCache.ps1 index 4ad31db274..a03d442bff 100644 --- a/scripts/ReplaceNugetCache.ps1 +++ b/scripts/ReplaceNugetCache.ps1 @@ -2,3 +2,4 @@ copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\ copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore.\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ diff --git a/scripts/ReplaceNugetCache.sh b/scripts/ReplaceNugetCache.sh index 636aec5f23..d50f4152e8 100755 --- a/scripts/ReplaceNugetCache.sh +++ b/scripts/ReplaceNugetCache.sh @@ -3,4 +3,6 @@ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/ cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.gtk3/$1/lib/netstandard2.0/ + cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/ + diff --git a/scripts/ReplaceNugetCacheRelease.ps1 b/scripts/ReplaceNugetCacheRelease.ps1 index f188c81c51..1c19e00400 100644 --- a/scripts/ReplaceNugetCacheRelease.ps1 +++ b/scripts/ReplaceNugetCacheRelease.ps1 @@ -1,4 +1,5 @@ copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\ copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\ copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\ -copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ \ No newline at end of file +copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\ +copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\ \ No newline at end of file diff --git a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs b/src/Android/Avalonia.Android/Resources/Resource.Designer.cs index e66c2800d3..80cbbc51ec 100644 --- a/src/Android/Avalonia.Android/Resources/Resource.Designer.cs +++ b/src/Android/Avalonia.Android/Resources/Resource.Designer.cs @@ -40,14 +40,11 @@ namespace Avalonia.Android public partial class String { - // aapt resource value: 0x7f020002 - public static int ApplicationName = 2130837506; - // aapt resource value: 0x7f020001 - public static int Hello = 2130837505; + public static int ApplicationName = 2130837505; // aapt resource value: 0x7f020000 - public static int library_name = 2130837504; + public static int Hello = 2130837504; static String() { diff --git a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs index 7973ad72e5..ad2cec2ae3 100644 --- a/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs +++ b/src/Android/Avalonia.AndroidTestApplication/MainActivity.cs @@ -56,7 +56,7 @@ namespace Avalonia.AndroidTestApplication { Margin = new Thickness(30), Background = Brushes.Yellow, - Children = new Avalonia.Controls.Controls + Children = { new TextBlock { diff --git a/src/Avalonia.Animation/Avalonia.Animation.csproj b/src/Avalonia.Animation/Avalonia.Animation.csproj index bafac2b261..2101c5669d 100644 --- a/src/Avalonia.Animation/Avalonia.Animation.csproj +++ b/src/Avalonia.Animation/Avalonia.Animation.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Animation.XML + bin\Debug\Avalonia.Animation.xml false @@ -21,7 +21,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Animation.XML + bin\Release\Avalonia.Animation.xml CS1591 true diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 58c510f483..54537841a9 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -12,7 +12,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Base.XML + bin\Debug\Avalonia.Base.xml CS1591 @@ -22,7 +22,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Base.XML + bin\Release\Avalonia.Base.xml CS1591 true diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index efcbb57244..17e6ea8f0f 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -51,6 +51,21 @@ namespace Avalonia /// private EventHandler _propertyChanged; + private DeferredSetter _directDeferredSetter; + + /// + /// Delayed setter helper for direct properties. Used to fix #855. + /// + private DeferredSetter DirectPropertyDeferredSetter + { + get + { + return _directDeferredSetter ?? + (_directDeferredSetter = new DeferredSetter()); + } + } + + /// /// Initializes a new instance of the class. /// @@ -225,6 +240,19 @@ namespace Avalonia return (T)GetValue((AvaloniaProperty)property); } + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + public bool IsAnimating(AvaloniaProperty property) + { + Contract.Requires(property != null); + VerifyAccess(); + + return _values.TryGetValue(property, out PriorityValue value) ? value.IsAnimating : false; + } + /// /// Checks whether a is set on this object. /// @@ -311,9 +339,6 @@ namespace Avalonia var description = GetDescription(source); - var scheduler = AvaloniaLocator.Current.GetService() ?? ImmediateScheduler.Instance; - source = source.ObserveOn(scheduler); - if (property.IsDirect) { if (property.IsReadOnly) @@ -539,6 +564,45 @@ namespace Avalonia } } + /// + /// A callback type for encapsulating complex logic for setting direct properties. + /// + /// The type of the property. + /// The value to which to set the property. + /// The backing field for the property. + /// A wrapper for the property-changed notification. + protected delegate void SetAndRaiseCallback(T value, ref T field, Action notifyWrapper); + + /// + /// Sets the backing field for a direct avalonia property, raising the + /// event if the value has changed. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// A callback called to actually set the value to the backing field. + /// The value. + /// + /// True if the value changed, otherwise false. + /// + protected bool SetAndRaise( + AvaloniaProperty property, + ref T field, + SetAndRaiseCallback setterCallback, + T value) + { + Contract.Requires(setterCallback != null); + return DirectPropertyDeferredSetter.SetAndNotify( + property, + ref field, + (object val, ref T backing, Action notify) => + { + setterCallback((T)val, ref backing, notify); + return true; + }, + value); + } + /// /// Sets the backing field for a direct avalonia property, raising the /// event if the value has changed. @@ -553,17 +617,32 @@ namespace Avalonia protected bool SetAndRaise(AvaloniaProperty property, ref T field, T value) { VerifyAccess(); - if (!object.Equals(field, value)) - { - var old = field; - field = value; - RaisePropertyChanged(property, old, value, BindingPriority.LocalValue); - return true; - } - else - { - return false; - } + return SetAndRaise( + property, + ref field, + (T val, ref T backing, Action notifyWrapper) + => SetAndRaiseCore(property, ref backing, val, notifyWrapper), + value); + } + + /// + /// Default assignment logic for SetAndRaise. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// A wrapper for the property-changed notification. + /// + /// True if the value changed, otherwise false. + /// + private bool SetAndRaiseCore(AvaloniaProperty property, ref T field, T value, Action notifyWrapper) + { + var old = field; + field = value; + + notifyWrapper(() => RaisePropertyChanged(property, old, value, BindingPriority.LocalValue)); + return true; } /// @@ -661,29 +740,41 @@ namespace Avalonia /// The value. private void SetDirectValue(AvaloniaProperty property, object value) { - var notification = value as BindingNotification; - - if (notification != null) + void Set() { - notification.LogIfError(this, property); - value = notification.Value; - } + var notification = value as BindingNotification; - if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue) - { - var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); - var accessor = (IDirectPropertyAccessor)GetRegistered(property); - var finalValue = value == AvaloniaProperty.UnsetValue ? - metadata.UnsetValue : value; + if (notification != null) + { + notification.LogIfError(this, property); + value = notification.Value; + } - LogPropertySet(property, value, BindingPriority.LocalValue); + if (notification == null || notification.ErrorType == BindingErrorType.Error || notification.HasValue) + { + var metadata = (IDirectPropertyMetadata)property.GetMetadata(GetType()); + var accessor = (IDirectPropertyAccessor)GetRegistered(property); + var finalValue = value == AvaloniaProperty.UnsetValue ? + metadata.UnsetValue : value; - accessor.SetValue(this, finalValue); + LogPropertySet(property, value, BindingPriority.LocalValue); + + accessor.SetValue(this, finalValue); + } + + if (notification != null) + { + UpdateDataValidation(property, notification); + } } - if (notification != null) + if (Dispatcher.UIThread.CheckAccess()) + { + Set(); + } + else { - UpdateDataValidation(property, notification); + Dispatcher.UIThread.InvokeAsync(Set); } } diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index e96679e643..1da2ecb942 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -138,17 +138,9 @@ namespace Avalonia AvaloniaProperty property, BindingPriority priority = BindingPriority.LocalValue) { - // TODO: Subject.Create is not yet in stable Rx : once it is, remove the - // AnonymousSubject classes and use Subject.Create. - var output = new Subject(); - var result = new AnonymousSubject( - Observer.Create( - x => output.OnNext(x), - e => output.OnError(e), - () => output.OnCompleted()), + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), o.GetObservable(property)); - o.Bind(property, output, priority); - return result; } /// @@ -169,17 +161,9 @@ namespace Avalonia AvaloniaProperty property, BindingPriority priority = BindingPriority.LocalValue) { - // TODO: Subject.Create is not yet in stable Rx : once it is, remove the - // AnonymousSubject classes from this file and use Subject.Create. - var output = new Subject(); - var result = new AnonymousSubject( - Observer.Create( - x => output.OnNext(x), - e => output.OnError(e), - () => output.OnCompleted()), + return Subject.Create( + Observer.Create(x => o.SetValue(property, x, priority)), o.GetObservable(property)); - o.Bind(property, output, priority); - return result; } /// diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index a3c3015a38..41c2ad6e54 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/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) { diff --git a/src/Avalonia.Base/Collections/IAvaloniaList.cs b/src/Avalonia.Base/Collections/IAvaloniaList.cs index 0233cee7a9..48c36976a5 100644 --- a/src/Avalonia.Base/Collections/IAvaloniaList.cs +++ b/src/Avalonia.Base/Collections/IAvaloniaList.cs @@ -36,6 +36,21 @@ namespace Avalonia.Collections /// The items. void InsertRange(int index, IEnumerable items); + /// + /// Moves an item to a new index. + /// + /// The index of the item to move. + /// The index to move the item to. + void Move(int oldIndex, int newIndex); + + /// + /// Moves multiple items to a new index. + /// + /// The first index of the items to move. + /// The number of items to move. + /// The index to move the items to. + void MoveRange(int oldIndex, int count, int newIndex); + /// /// Removes multiple items from the collection. /// diff --git a/src/Avalonia.Base/IAvaloniaObject.cs b/src/Avalonia.Base/IAvaloniaObject.cs index c11bab2236..c11f8ada7e 100644 --- a/src/Avalonia.Base/IAvaloniaObject.cs +++ b/src/Avalonia.Base/IAvaloniaObject.cs @@ -31,6 +31,13 @@ namespace Avalonia /// The value. T GetValue(AvaloniaProperty property); + /// + /// Checks whether a is animating. + /// + /// The property. + /// True if the property is animating, otherwise false. + bool IsAnimating(AvaloniaProperty property); + /// /// Checks whether a is set on this object. /// diff --git a/src/Avalonia.Base/PriorityBindingEntry.cs b/src/Avalonia.Base/PriorityBindingEntry.cs index 63f582a34c..b44b845f25 100644 --- a/src/Avalonia.Base/PriorityBindingEntry.cs +++ b/src/Avalonia.Base/PriorityBindingEntry.cs @@ -3,6 +3,7 @@ using System; using Avalonia.Data; +using Avalonia.Threading; namespace Avalonia { @@ -92,33 +93,50 @@ namespace Avalonia private void ValueChanged(object value) { - _owner.Owner.Owner?.VerifyAccess(); - - var notification = value as BindingNotification; - - if (notification != null) + void Signal() { - if (notification.HasValue || notification.ErrorType == BindingErrorType.Error) + var notification = value as BindingNotification; + + if (notification != null) { - Value = notification.Value; - _owner.Changed(this); + if (notification.HasValue || notification.ErrorType == BindingErrorType.Error) + { + Value = notification.Value; + _owner.Changed(this); + } + + if (notification.ErrorType != BindingErrorType.None) + { + _owner.Error(this, notification); + } } - - if (notification.ErrorType != BindingErrorType.None) + else { - _owner.Error(this, notification); + Value = value; + _owner.Changed(this); } } + + if (Dispatcher.UIThread.CheckAccess()) + { + Signal(); + } else { - Value = value; - _owner.Changed(this); + Dispatcher.UIThread.InvokeAsync(Signal); } } private void Completed() { - _owner.Completed(this); + if (Dispatcher.UIThread.CheckAccess()) + { + _owner.Completed(this); + } + else + { + Dispatcher.UIThread.InvokeAsync(() => _owner.Completed(this)); + } } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index 3726fb7ae5..9b5318083a 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -28,8 +28,10 @@ namespace Avalonia { private readonly Type _valueType; private readonly SingleOrDictionary _levels = new SingleOrDictionary(); - private object _value; + private readonly Func _validate; + private static readonly DeferredSetter delayedSetter = new DeferredSetter(); + private (object value, int priority) _value; /// /// Initializes a new instance of the class. @@ -47,11 +49,22 @@ namespace Avalonia Owner = owner; Property = property; _valueType = valueType; - _value = AvaloniaProperty.UnsetValue; - ValuePriority = int.MaxValue; + _value = (AvaloniaProperty.UnsetValue, int.MaxValue); _validate = validate; } + /// + /// Gets a value indicating whether the property is animating. + /// + public bool IsAnimating + { + get + { + return ValuePriority <= (int)BindingPriority.Animation && + GetLevel(ValuePriority).ActiveBindingIndex != -1; + } + } + /// /// Gets the owner of the value. /// @@ -65,16 +78,12 @@ namespace Avalonia /// /// Gets the current value. /// - public object Value => _value; + public object Value => _value.value; /// /// Gets the priority of the binding that is currently active. /// - public int ValuePriority - { - get; - private set; - } + public int ValuePriority => _value.priority; /// /// Adds a new binding. @@ -234,25 +243,36 @@ namespace Avalonia /// The priority level that the value came from. private void UpdateValue(object value, int priority) { - var notification = value as BindingNotification; + delayedSetter.SetAndNotify(this, + ref _value, + UpdateCore, + (value, priority)); + } + + private bool UpdateCore( + (object value, int priority) update, + ref (object value, int priority) backing, + Action notify) + { + var val = update.value; + var notification = val as BindingNotification; object castValue; if (notification != null) { - value = (notification.HasValue) ? notification.Value : null; + val = (notification.HasValue) ? notification.Value : null; } - if (TypeUtilities.TryConvertImplicit(_valueType, value, out castValue)) + if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue)) { - var old = _value; + var old = backing.value; if (_validate != null && castValue != AvaloniaProperty.UnsetValue) { castValue = _validate(castValue); } - ValuePriority = priority; - _value = castValue; + backing = (castValue, update.priority); if (notification?.HasValue == true) { @@ -261,7 +281,7 @@ namespace Avalonia if (notification == null || notification.HasValue) { - Owner?.Changed(this, old, _value); + notify(() => Owner?.Changed(this, old, Value)); } if (notification != null) @@ -272,14 +292,15 @@ namespace Avalonia else { Logger.Error( - LogArea.Binding, + LogArea.Binding, Owner, "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})", - Property.Name, - _valueType, - value, - value?.GetType()); + Property.Name, + _valueType, + val, + val?.GetType()); } + return true; } } } diff --git a/src/Avalonia.Base/Reactive/AnonymousSubject`1.cs b/src/Avalonia.Base/Reactive/AnonymousSubject`1.cs deleted file mode 100644 index c7bed15ecb..0000000000 --- a/src/Avalonia.Base/Reactive/AnonymousSubject`1.cs +++ /dev/null @@ -1,16 +0,0 @@ -// 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; -using System.Reactive.Subjects; - -namespace Avalonia.Reactive -{ - public class AnonymousSubject : AnonymousSubject, ISubject - { - public AnonymousSubject(IObserver observer, IObservable observable) - : base(observer, observable) - { - } - } -} diff --git a/src/Avalonia.Base/Reactive/AnonymousSubject`2.cs b/src/Avalonia.Base/Reactive/AnonymousSubject`2.cs deleted file mode 100644 index 18551d564e..0000000000 --- a/src/Avalonia.Base/Reactive/AnonymousSubject`2.cs +++ /dev/null @@ -1,49 +0,0 @@ -// 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; -using System.Reactive.Subjects; - -namespace Avalonia.Reactive -{ - public class AnonymousSubject : ISubject - { - private readonly IObserver _observer; - private readonly IObservable _observable; - - public AnonymousSubject(IObserver observer, IObservable observable) - { - _observer = observer; - _observable = observable; - } - - public void OnCompleted() - { - _observer.OnCompleted(); - } - - public void OnError(Exception error) - { - if (error == null) - throw new ArgumentNullException("error"); - - _observer.OnError(error); - } - - public void OnNext(T value) - { - _observer.OnNext(value); - } - - public IDisposable Subscribe(IObserver observer) - { - if (observer == null) - throw new ArgumentNullException("observer"); - - // - // [OK] Use of unsafe Subscribe: non-pretentious wrapping of an observable sequence. - // - return _observable.Subscribe/*Unsafe*/(observer); - } - } -} diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs new file mode 100644 index 0000000000..fdfa160134 --- /dev/null +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -0,0 +1,156 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Disposables; +using System.Runtime.CompilerServices; +using System.Text; + +namespace Avalonia.Utilities +{ + /// + /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// + /// The type of the object that represents the property. + /// The type of value with which to track the delayed assignment. + class DeferredSetter + where TProperty: class + { + private struct NotifyDisposable : IDisposable + { + private readonly SettingStatus status; + + internal NotifyDisposable(SettingStatus status) + { + this.status = status; + status.Notifying = true; + } + + public void Dispose() + { + status.Notifying = false; + } + } + + /// + /// Information on current setting/notification status of a property. + /// + private class SettingStatus + { + public bool Notifying { get; set; } + + private Queue pendingValues; + + public Queue PendingValues + { + get + { + return pendingValues ?? (pendingValues = new Queue()); + } + } + } + + private readonly ConditionalWeakTable setRecords = new ConditionalWeakTable(); + + /// + /// Mark the property as currently notifying. + /// + /// The property to mark as notifying. + /// Returns a disposable that when disposed, marks the property as done notifying. + private NotifyDisposable MarkNotifying(TProperty property) + { + Contract.Requires(!IsNotifying(property)); + + return new NotifyDisposable(setRecords.GetOrCreateValue(property)); + } + + /// + /// Check if the property is currently notifying listeners. + /// + /// The property. + /// If the property is currently notifying listeners. + private bool IsNotifying(TProperty property) + => setRecords.TryGetValue(property, out var value) && value.Notifying; + + /// + /// Add a pending assignment for the property. + /// + /// The property. + /// The value to assign. + private void AddPendingSet(TProperty property, TSetRecord value) + { + Contract.Requires(IsNotifying(property)); + + setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value); + } + + /// + /// Checks if there are any pending assignments for the property. + /// + /// The property to check. + /// If the property has any pending assignments. + private bool HasPendingSet(TProperty property) + { + return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0; + } + + /// + /// Gets the first pending assignment for the property. + /// + /// The property to check. + /// The first pending assignment for the property. + private TSetRecord GetFirstPendingSet(TProperty property) + { + return setRecords.GetOrCreateValue(property).PendingValues.Dequeue(); + } + + public delegate bool SetterDelegate(TSetRecord record, ref TValue backing, Action notifyCallback); + + /// + /// Set the property and notify listeners while ensuring we don't get into a stack overflow as happens with #855 and #824 + /// + /// The property to set. + /// The backing field for the property + /// + /// A callback that actually sets the property. + /// The first parameter is the value to set, and the second is a wrapper that takes a callback that sends the property-changed notification. + /// + /// The value to try to set. + public bool SetAndNotify( + TProperty property, + ref TValue backing, + SetterDelegate setterCallback, + TSetRecord value) + { + Contract.Requires(setterCallback != null); + if (!IsNotifying(property)) + { + bool updated = false; + if (!object.Equals(value, backing)) + { + updated = setterCallback(value, ref backing, notification => + { + using (MarkNotifying(property)) + { + notification(); + } + }); + } + while (HasPendingSet(property)) + { + updated |= setterCallback(GetFirstPendingSet(property), ref backing, notification => + { + using (MarkNotifying(property)) + { + notification(); + } + }); + } + return updated; + } + else if(!object.Equals(value, backing)) + { + AddPendingSet(property, value); + } + return false; + } + } +} diff --git a/src/Avalonia.Controls/AppBuilderBase.cs b/src/Avalonia.Controls/AppBuilderBase.cs index 1610d33f9b..7af3deef34 100644 --- a/src/Avalonia.Controls/AppBuilderBase.cs +++ b/src/Avalonia.Controls/AppBuilderBase.cs @@ -14,6 +14,8 @@ namespace Avalonia.Controls /// The type of the AppBuilder class itself. public abstract class AppBuilderBase where TAppBuilder : AppBuilderBase, new() { + private static bool s_setupWasAlreadyCalled; + /// /// Gets or sets the instance. /// @@ -207,6 +209,17 @@ namespace Avalonia.Controls public TAppBuilder UseAvaloniaModules() => AfterSetup(builder => SetupAvaloniaModules()); + private bool CheckSetup { get; set; } = true; + + /// + /// Set this AppBuilder to ignore the setup check. Used for testing purposes. + /// + internal TAppBuilder IgnoreSetupCheck() + { + CheckSetup = false; + return Self; + } + private void SetupAvaloniaModules() { var moduleInitializers = from assembly in AvaloniaLocator.Current.GetService().GetLoadedAssemblies() @@ -252,6 +265,13 @@ namespace Avalonia.Controls throw new InvalidOperationException("No rendering system configured."); } + if (s_setupWasAlreadyCalled && CheckSetup) + { + throw new InvalidOperationException("Setup was already called on one of AppBuilder instances"); + } + + s_setupWasAlreadyCalled = true; + Instance.RegisterServices(); RuntimePlatformServicesInitializer(); WindowingSubsystemInitializer(); diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs index abae080515..06c1a8b4cc 100644 --- a/src/Avalonia.Controls/Application.cs +++ b/src/Avalonia.Controls/Application.cs @@ -175,6 +175,15 @@ namespace Avalonia closable.Closed += (s, e) => source.Cancel(); Dispatcher.UIThread.MainLoop(source.Token); } + + /// + /// Runs the application's main loop until the is cancelled. + /// + /// The token to track + public void Run(CancellationToken token) + { + Dispatcher.UIThread.MainLoop(token); + } /// /// Exits the application diff --git a/src/Avalonia.Controls/Avalonia.Controls.csproj b/src/Avalonia.Controls/Avalonia.Controls.csproj index c8f6b4e65a..997e15050f 100644 --- a/src/Avalonia.Controls/Avalonia.Controls.csproj +++ b/src/Avalonia.Controls/Avalonia.Controls.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Controls.XML + bin\Debug\Avalonia.Controls.xml CS1591;CS0067 @@ -21,7 +21,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Controls.XML + bin\Release\Avalonia.Controls.xml CS1591 true diff --git a/src/Avalonia.Controls/Calendar/Calendar.cs b/src/Avalonia.Controls/Calendar/Calendar.cs index 8c79e5dce5..59281c5ad0 100644 --- a/src/Avalonia.Controls/Calendar/Calendar.cs +++ b/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) { diff --git a/src/Avalonia.Controls/ContentControl.cs b/src/Avalonia.Controls/ContentControl.cs index 0eba3c13ae..6da6da54a5 100644 --- a/src/Avalonia.Controls/ContentControl.cs +++ b/src/Avalonia.Controls/ContentControl.cs @@ -51,6 +51,7 @@ namespace Avalonia.Controls /// Gets or sets the content to display. /// [Content] + [DependsOn(nameof(ContentTemplate))] public object Content { get { return GetValue(ContentProperty); } diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index d4777b2f8a..89721e2e05 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -487,11 +487,6 @@ namespace Avalonia.Controls void ILogical.NotifyResourcesChanged(ResourcesChangedEventArgs e) { ResourcesChanged?.Invoke(this, new ResourcesChangedEventArgs()); - - foreach (var child in LogicalChildren) - { - child.NotifyResourcesChanged(e); - } } /// @@ -536,6 +531,15 @@ namespace Avalonia.Controls } _parent = (IControl)parent; + + if (old != null) + { + old.ResourcesChanged -= ThisResourcesChanged; + } + if (_parent != null) + { + _parent.ResourcesChanged += ThisResourcesChanged; + } ((ILogical)this).NotifyResourcesChanged(new ResourcesChangedEventArgs()); if (_parent is IStyleRoot || _parent?.IsAttachedToLogicalTree == true || this is IStyleRoot) @@ -621,7 +625,6 @@ namespace Avalonia.Controls Contract.Requires(property != null); Contract.Requires(selector != null); Contract.Requires(className != null); - Contract.Requires(property != null); if (string.IsNullOrWhiteSpace(className)) { diff --git a/src/Avalonia.Controls/DropDown.cs b/src/Avalonia.Controls/DropDown.cs index fa2e0c1e16..6b27c479ba 100644 --- a/src/Avalonia.Controls/DropDown.cs +++ b/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); + } + } + /// 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; + } + } } } diff --git a/src/Avalonia.Controls/IPanel.cs b/src/Avalonia.Controls/IPanel.cs index 5bcfd33e86..ce2c63ffc8 100644 --- a/src/Avalonia.Controls/IPanel.cs +++ b/src/Avalonia.Controls/IPanel.cs @@ -9,8 +9,8 @@ namespace Avalonia.Controls public interface IPanel : IControl { /// - /// Gets or sets the children of the . + /// Gets the children of the . /// - Controls Children { get; set; } + Controls Children { get; } } } \ No newline at end of file diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index aa209e0462..4366de1cd6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/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; + } + /// /// Gets or sets the panel used to display the items. /// @@ -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); diff --git a/src/Avalonia.Controls/Panel.cs b/src/Avalonia.Controls/Panel.cs index 3272d3779b..a2cb013300 100644 --- a/src/Avalonia.Controls/Panel.cs +++ b/src/Avalonia.Controls/Panel.cs @@ -25,8 +25,6 @@ namespace Avalonia.Controls public static readonly StyledProperty BackgroundProperty = Border.BackgroundProperty.AddOwner(); - private readonly Controls _children = new Controls(); - /// /// Initializes static members of the class. /// @@ -40,38 +38,14 @@ namespace Avalonia.Controls /// public Panel() { - _children.CollectionChanged += ChildrenChanged; + Children.CollectionChanged += ChildrenChanged; } /// - /// Gets or sets the children of the . + /// Gets the children of the . /// - /// - /// Even though this property can be set, the setter is only intended for use in object - /// initializers. Assigning to this property does not change the underlying collection, - /// it simply clears the existing collection and adds the contents of the assigned - /// collection. - /// [Content] - public Controls Children - { - get - { - return _children; - } - - set - { - Contract.Requires(value != null); - - if (_children != value) - { - VisualChildren.Clear(); - _children.Clear(); - _children.AddRange(value); - } - } - } + public Controls Children { get; } = new Controls(); /// /// Gets or Sets Panel background brush. @@ -115,6 +89,11 @@ namespace Avalonia.Controls VisualChildren.AddRange(e.NewItems.OfType()); 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().ToList(); LogicalChildren.RemoveAll(controls); @@ -132,11 +111,7 @@ namespace Avalonia.Controls break; case NotifyCollectionChangedAction.Reset: - controls = e.OldItems.OfType().ToList(); - LogicalChildren.Clear(); - VisualChildren.Clear(); - VisualChildren.AddRange(_children); - break; + throw new NotSupportedException(); } InvalidateMeasure(); diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index c1adff402a..a97fdf8784 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.VisualTree; namespace Avalonia.Controls.Presenters @@ -139,6 +140,7 @@ namespace Avalonia.Controls.Presenters /// /// Gets or sets the content to be displayed by the presenter. /// + [DependsOn(nameof(ContentTemplate))] public object Content { get { return GetValue(ContentProperty); } @@ -255,18 +257,9 @@ namespace Avalonia.Controls.Presenters LogicalChildren.Remove(oldChild); } - if (newChild.Parent == null) + if (newChild.Parent == null && TemplatedParent == null) { - var templatedLogicalParent = TemplatedParent as ILogical; - - if (templatedLogicalParent != null) - { - ((ISetLogicalParent)newChild).SetParent(templatedLogicalParent); - } - else - { - LogicalChildren.Add(newChild); - } + LogicalChildren.Add(newChild); } VisualChildren.Add(newChild); diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index a999e4ae37..507a085fed 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -8,6 +8,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.Styling; using Avalonia.VisualTree; using JetBrains.Annotations; @@ -16,7 +17,7 @@ namespace Avalonia.Controls.Primitives /// /// The root window of a . /// - public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable + public class PopupRoot : WindowBase, IInteractive, IHostedVisualTreeRoot, IDisposable, IStyleHost { private IDisposable _presenterSubscription; @@ -66,6 +67,11 @@ namespace Avalonia.Controls.Primitives /// IVisual IHostedVisualTreeRoot.Host => Parent; + /// + /// Gets the styling parent of the popup root. + /// + IStyleHost IStyleHost.StylingParent => Parent; + /// public void Dispose() => PlatformImpl?.Dispose(); @@ -90,20 +96,23 @@ namespace Avalonia.Controls.Primitives private void SetTemplatedParentAndApplyChildTemplates(IControl control) { - var templatedParent = Parent.TemplatedParent; - - if (control.TemplatedParent == null) + if (control != null) { - control.SetValue(TemplatedParentProperty, templatedParent); - } + var templatedParent = Parent.TemplatedParent; - control.ApplyTemplate(); + if (control.TemplatedParent == null) + { + control.SetValue(TemplatedParentProperty, templatedParent); + } - if (!(control is IPresenter) && control.TemplatedParent == templatedParent) - { - foreach (IControl child in control.GetVisualChildren()) + control.ApplyTemplate(); + + if (!(control is IPresenter) && control.TemplatedParent == templatedParent) { - SetTemplatedParentAndApplyChildTemplates(child); + foreach (IControl child in control.GetVisualChildren()) + { + SetTemplatedParentAndApplyChildTemplates(child); + } } } } diff --git a/src/Avalonia.Controls/Primitives/RangeBase.cs b/src/Avalonia.Controls/Primitives/RangeBase.cs index b69dbba637..b46562e99e 100644 --- a/src/Avalonia.Controls/Primitives/RangeBase.cs +++ b/src/Avalonia.Controls/Primitives/RangeBase.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using Avalonia.Data; using Avalonia.Utilities; namespace Avalonia.Controls.Primitives @@ -36,7 +37,8 @@ namespace Avalonia.Controls.Primitives AvaloniaProperty.RegisterDirect( nameof(Value), o => o.Value, - (o, v) => o.Value = v); + (o, v) => o.Value = v, + defaultBindingMode: BindingMode.TwoWay); /// /// Defines the property. diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index ab09a4701d..2e668fda95 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -151,15 +151,23 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - var old = SelectedIndex; - var effective = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; - - if (old != effective) + SetAndRaise(SelectedIndexProperty, ref _selectedIndex, (int val, ref int backing, Action notifyWrapper) => { - _selectedIndex = effective; - RaisePropertyChanged(SelectedIndexProperty, old, effective, BindingPriority.LocalValue); - SelectedItem = ElementAt(Items, effective); - } + var old = backing; + var effective = (val >= 0 && val < Items?.Cast().Count()) ? val : -1; + + if (old != effective) + { + backing = effective; + notifyWrapper(() => + RaisePropertyChanged( + SelectedIndexProperty, + old, + effective, + BindingPriority.LocalValue)); + SelectedItem = ElementAt(Items, effective); + } + }, value); } else { @@ -183,31 +191,41 @@ namespace Avalonia.Controls.Primitives { if (_updateCount == 0) { - var old = SelectedItem; - var index = IndexOf(Items, value); - var effective = index != -1 ? value : null; - - if (!object.Equals(effective, old)) + SetAndRaise(SelectedItemProperty, ref _selectedItem, (object val, ref object backing, Action notifyWrapper) => { - _selectedItem = effective; - RaisePropertyChanged(SelectedItemProperty, old, effective, BindingPriority.LocalValue); - SelectedIndex = index; + var old = backing; + var index = IndexOf(Items, val); + var effective = index != -1 ? val : null; - if (effective != null) + if (!object.Equals(effective, old)) { - if (SelectedItems.Count != 1 || SelectedItems[0] != effective) + backing = effective; + + notifyWrapper(() => + RaisePropertyChanged( + SelectedItemProperty, + old, + effective, + BindingPriority.LocalValue)); + + SelectedIndex = index; + + if (effective != null) + { + if (SelectedItems.Count != 1 || SelectedItems[0] != effective) + { + _syncingSelectedItems = true; + SelectedItems.Clear(); + SelectedItems.Add(effective); + _syncingSelectedItems = false; + } + } + else if (SelectedItems.Count > 0) { - _syncingSelectedItems = true; SelectedItems.Clear(); - SelectedItems.Add(effective); - _syncingSelectedItems = false; } } - else if (SelectedItems.Count > 0) - { - SelectedItems.Clear(); - } - } + }, value); } else { @@ -297,7 +315,7 @@ namespace Avalonia.Controls.Primitives .OfType() .FirstOrDefault(x => x.LogicalParent == this && ItemContainerGenerator?.IndexFromContainer(x) != -1); - return item as IControl; + return item; } /// diff --git a/src/Avalonia.Controls/Primitives/Track.cs b/src/Avalonia.Controls/Primitives/Track.cs index 8c577665d5..648fe5f4b0 100644 --- a/src/Avalonia.Controls/Primitives/Track.cs +++ b/src/Avalonia.Controls/Primitives/Track.cs @@ -154,7 +154,11 @@ namespace Avalonia.Controls.Primitives if (increaseButton != null) { - increaseButton.Arrange(new Rect(firstWidth + thumbWidth, 0, remaining - firstWidth, finalSize.Height)); + increaseButton.Arrange(new Rect( + firstWidth + thumbWidth, + 0, + Math.Max(0, remaining - firstWidth), + finalSize.Height)); } } else @@ -185,7 +189,11 @@ namespace Avalonia.Controls.Primitives if (increaseButton != null) { - increaseButton.Arrange(new Rect(0, firstHeight + thumbHeight, finalSize.Width, Math.Max(remaining - firstHeight, 0))); + increaseButton.Arrange(new Rect( + 0, + firstHeight + thumbHeight, + finalSize.Width, + Math.Max(remaining - firstHeight, 0))); } } diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index de02c10764..8cf6b149cb 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -28,9 +28,6 @@ namespace Avalonia.Controls { ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); - HorizontalAlignmentProperty.OverrideDefaultValue(HorizontalAlignment.Left); - VerticalAlignmentProperty.OverrideDefaultValue(VerticalAlignment.Top); - IsIndeterminateProperty.Changed.AddClassHandler( (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); OrientationProperty.Changed.AddClassHandler( diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 67a45140c8..36ef8d05c3 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -178,6 +178,10 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); SetAndRaise(SelectionStartProperty, ref _selectionStart, value); + if (SelectionStart == SelectionEnd) + { + CaretIndex = SelectionStart; + } } } @@ -192,6 +196,10 @@ namespace Avalonia.Controls { value = CoerceCaretIndex(value); SetAndRaise(SelectionEndProperty, ref _selectionEnd, value); + if (SelectionStart == SelectionEnd) + { + CaretIndex = SelectionEnd; + } } } diff --git a/src/Avalonia.Controls/TreeView.cs b/src/Avalonia.Controls/TreeView.cs index 079e571d29..fa3ecdedef 100644 --- a/src/Avalonia.Controls/TreeView.cs +++ b/src/Avalonia.Controls/TreeView.cs @@ -176,10 +176,7 @@ namespace Avalonia.Controls SelectedItem = item; - if (SelectedItem != null) - { - MarkContainerSelected(container, true); - } + MarkContainerSelected(container, true); } } diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index 834f6d218b..409dd231ad 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/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 { diff --git a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj index abbdd15bed..5f8e25269b 100644 --- a/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj +++ b/src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj @@ -11,7 +11,7 @@ TRACE;DEBUG prompt 4 - bin\Debug\Avalonia.Diagnostics.XML + bin\Debug\Avalonia.Diagnostics.xml CS1591 @@ -21,7 +21,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Diagnostics.XML + bin\Release\Avalonia.Diagnostics.xml CS1591 true diff --git a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs index 7e4e5a8564..d445f1cd70 100644 --- a/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs +++ b/src/Avalonia.Diagnostics/Views/TreePage.xaml.cs @@ -27,6 +27,12 @@ namespace Avalonia.Diagnostics.Views if (layer != null) { + if (_adorner != null) + { + ((Panel)_adorner.Parent).Children.Remove(_adorner); + _adorner = null; + } + _adorner = new Rectangle { Fill = new SolidColorBrush(0x80a0c5e8), diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index 53b2c997d0..8630e7b228 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -5,7 +5,7 @@ $(DefineConstants);DOTNETCORE - bin\$(Configuration)\Avalonia.DotNetCoreRuntime.XML + bin\$(Configuration)\Avalonia.DotNetCoreRuntime.xml diff --git a/src/Avalonia.Input/Avalonia.Input.csproj b/src/Avalonia.Input/Avalonia.Input.csproj index 901f7b5675..c82601ac2c 100644 --- a/src/Avalonia.Input/Avalonia.Input.csproj +++ b/src/Avalonia.Input/Avalonia.Input.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Input.XML + bin\Debug\Avalonia.Input.xml CS1591 @@ -21,7 +21,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Input.XML + bin\Release\Avalonia.Input.xml CS1591 true diff --git a/src/Avalonia.Input/KeyGesture.cs b/src/Avalonia.Input/KeyGesture.cs index 88d3c99839..954333b80f 100644 --- a/src/Avalonia.Input/KeyGesture.cs +++ b/src/Avalonia.Input/KeyGesture.cs @@ -111,6 +111,21 @@ namespace Avalonia.Input return string.Join(" + ", parts); } - public bool Matches(KeyEventArgs keyEvent) => keyEvent.Key == Key && keyEvent.Modifiers == Modifiers; + public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.Modifiers == Modifiers; + + private Key ResolveNumPadOperationKey(Key key) + { + switch (key) + { + case Key.Add: + return Key.OemPlus; + case Key.Subtract: + return Key.OemMinus; + case Key.Decimal: + return Key.OemPeriod; + default: + return key; + } + } } } diff --git a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj index d683c77468..4f6f9ad6fe 100644 --- a/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj +++ b/src/Avalonia.Interactivity/Avalonia.Interactivity.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Interactivity.XML + bin\Debug\Avalonia.Interactivity.xml CS1591 @@ -21,7 +21,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Interactivity.XML + bin\Release\Avalonia.Interactivity.xml CS1591 true diff --git a/src/Avalonia.Layout/Avalonia.Layout.csproj b/src/Avalonia.Layout/Avalonia.Layout.csproj index 810a985e36..58c18921f5 100644 --- a/src/Avalonia.Layout/Avalonia.Layout.csproj +++ b/src/Avalonia.Layout/Avalonia.Layout.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Layout.XML + bin\Debug\Avalonia.Layout.xml CS1591 @@ -21,7 +21,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Layout.XML + bin\Release\Avalonia.Layout.xml CS1591 true diff --git a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj b/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj index 5aa2cd67ba..4d9d141d0d 100644 --- a/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj +++ b/src/Avalonia.Logging.Serilog/Avalonia.Logging.Serilog.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Logging.Serilog.XML + bin\Debug\Avalonia.Logging.Serilog.xml pdbonly @@ -20,7 +20,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Logging.Serilog.XML + bin\Release\Avalonia.Logging.Serilog.xml true diff --git a/src/Avalonia.Styling/Avalonia.Styling.csproj b/src/Avalonia.Styling/Avalonia.Styling.csproj index 6c0b957bf4..b8083b52db 100644 --- a/src/Avalonia.Styling/Avalonia.Styling.csproj +++ b/src/Avalonia.Styling/Avalonia.Styling.csproj @@ -12,7 +12,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Styling.XML + bin\Debug\Avalonia.Styling.xml CS1591 @@ -22,7 +22,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Styling.XML + bin\Release\Avalonia.Styling.xml CS1591 true diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index b4b200834b..d8260226e9 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Themes.Default.XML + bin\Debug\Avalonia.Themes.Default.xml pdbonly @@ -20,7 +20,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Themes.Default.XML + bin\Release\Avalonia.Themes.Default.xml true diff --git a/src/Avalonia.Themes.Default/Button.xaml b/src/Avalonia.Themes.Default/Button.xaml index 908f293fa7..94fcce5e0c 100644 --- a/src/Avalonia.Themes.Default/Button.xaml +++ b/src/Avalonia.Themes.Default/Button.xaml @@ -13,8 +13,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" diff --git a/src/Avalonia.Themes.Default/CalendarButton.xaml b/src/Avalonia.Themes.Default/CalendarButton.xaml index 5f3cba5c7a..84969c135f 100644 --- a/src/Avalonia.Themes.Default/CalendarButton.xaml +++ b/src/Avalonia.Themes.Default/CalendarButton.xaml @@ -29,8 +29,8 @@ diff --git a/src/Avalonia.Themes.Default/ContentControl.xaml b/src/Avalonia.Themes.Default/ContentControl.xaml index 27f6648548..7584ae0dfe 100644 --- a/src/Avalonia.Themes.Default/ContentControl.xaml +++ b/src/Avalonia.Themes.Default/ContentControl.xaml @@ -5,8 +5,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/DropDownItem.xaml b/src/Avalonia.Themes.Default/DropDownItem.xaml index 48d791f629..257030d8af 100644 --- a/src/Avalonia.Themes.Default/DropDownItem.xaml +++ b/src/Avalonia.Themes.Default/DropDownItem.xaml @@ -10,8 +10,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml index f39720bb65..de364c0108 100644 --- a/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml +++ b/src/Avalonia.Themes.Default/EmbeddableControlRoot.xaml @@ -7,8 +7,8 @@ diff --git a/src/Avalonia.Themes.Default/Expander.xaml b/src/Avalonia.Themes.Default/Expander.xaml index 51efec6b95..5e05dcf608 100644 --- a/src/Avalonia.Themes.Default/Expander.xaml +++ b/src/Avalonia.Themes.Default/Expander.xaml @@ -16,8 +16,8 @@ @@ -34,8 +34,8 @@ @@ -52,8 +52,8 @@ @@ -70,8 +70,8 @@ diff --git a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml b/src/Avalonia.Themes.Default/LayoutTransformControl.xaml index c9df62625f..b26f053622 100644 --- a/src/Avalonia.Themes.Default/LayoutTransformControl.xaml +++ b/src/Avalonia.Themes.Default/LayoutTransformControl.xaml @@ -5,8 +5,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/ListBoxItem.xaml b/src/Avalonia.Themes.Default/ListBoxItem.xaml index 64fc62ba73..a0405f2875 100644 --- a/src/Avalonia.Themes.Default/ListBoxItem.xaml +++ b/src/Avalonia.Themes.Default/ListBoxItem.xaml @@ -7,8 +7,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/PopupRoot.xaml b/src/Avalonia.Themes.Default/PopupRoot.xaml index 47e519bf90..cc23367ac0 100644 --- a/src/Avalonia.Themes.Default/PopupRoot.xaml +++ b/src/Avalonia.Themes.Default/PopupRoot.xaml @@ -4,8 +4,8 @@ diff --git a/src/Avalonia.Themes.Default/RadioButton.xaml b/src/Avalonia.Themes.Default/RadioButton.xaml index 0a0eb48564..fb71432595 100644 --- a/src/Avalonia.Themes.Default/RadioButton.xaml +++ b/src/Avalonia.Themes.Default/RadioButton.xaml @@ -29,8 +29,8 @@ HorizontalAlignment="Center" VerticalAlignment="Center"/> diff --git a/src/Avalonia.Themes.Default/RepeatButton.xaml b/src/Avalonia.Themes.Default/RepeatButton.xaml index 1caaa266de..872f390ac5 100644 --- a/src/Avalonia.Themes.Default/RepeatButton.xaml +++ b/src/Avalonia.Themes.Default/RepeatButton.xaml @@ -20,8 +20,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" diff --git a/src/Avalonia.Themes.Default/TabStripItem.xaml b/src/Avalonia.Themes.Default/TabStripItem.xaml index 7f7fef0a1a..408cfaa3d7 100644 --- a/src/Avalonia.Themes.Default/TabStripItem.xaml +++ b/src/Avalonia.Themes.Default/TabStripItem.xaml @@ -9,8 +9,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/ToggleButton.xaml b/src/Avalonia.Themes.Default/ToggleButton.xaml index 12d7daacda..dadc45d854 100644 --- a/src/Avalonia.Themes.Default/ToggleButton.xaml +++ b/src/Avalonia.Themes.Default/ToggleButton.xaml @@ -13,8 +13,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" TextBlock.Foreground="{TemplateBinding Foreground}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" diff --git a/src/Avalonia.Themes.Default/ToolTip.xaml b/src/Avalonia.Themes.Default/ToolTip.xaml index f3e819c101..1fc0202dd3 100644 --- a/src/Avalonia.Themes.Default/ToolTip.xaml +++ b/src/Avalonia.Themes.Default/ToolTip.xaml @@ -9,8 +9,8 @@ Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" - Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" + Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 06922addad..d19bf41c4d 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -7,8 +7,8 @@ diff --git a/src/Avalonia.Visuals/Media/PathMarkupParser.cs b/src/Avalonia.Visuals/Media/PathMarkupParser.cs index fbc189546c..9e4a3cbeae 100644 --- a/src/Avalonia.Visuals/Media/PathMarkupParser.cs +++ b/src/Avalonia.Visuals/Media/PathMarkupParser.cs @@ -320,7 +320,7 @@ namespace Avalonia.Media if (c == 'E') { readSign = false; - readExponent = c == 'E'; + readExponent = true; } } else diff --git a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs index f7befa646a..041d8f8f6b 100644 --- a/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/DeferredRenderer.cs @@ -25,11 +25,9 @@ namespace Avalonia.Rendering private readonly IRenderLoop _renderLoop; private readonly IVisual _root; private readonly ISceneBuilder _sceneBuilder; - private readonly RenderLayers _layers; private bool _running; private Scene _scene; - private IRenderTarget _renderTarget; private DirtyVisuals _dirty; private IRenderTargetBitmapImpl _overlay; private bool _updateQueued; @@ -56,7 +54,7 @@ namespace Avalonia.Rendering _dispatcher = dispatcher ?? Dispatcher.UIThread; _root = root; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layers = new RenderLayers(); + Layers = new RenderLayers(); _renderLoop = renderLoop; } @@ -78,9 +76,9 @@ namespace Avalonia.Rendering Contract.Requires(renderTarget != null); _root = root; - _renderTarget = renderTarget; + RenderTarget = renderTarget; _sceneBuilder = sceneBuilder ?? new SceneBuilder(); - _layers = new RenderLayers(); + Layers = new RenderLayers(); } /// @@ -94,6 +92,16 @@ namespace Avalonia.Rendering /// public string DebugFramesPath { get; set; } + /// + /// Gets the render layers. + /// + internal RenderLayers Layers { get; } + + /// + /// Gets the current render target. + /// + internal IRenderTarget RenderTarget { get; private set; } + /// public void AddDirty(IVisual visual) { @@ -173,9 +181,9 @@ namespace Avalonia.Rendering bool renderOverlay = DrawDirtyRects || DrawFps; bool composite = false; - if (_renderTarget == null) + if (RenderTarget == null) { - _renderTarget = ((IRenderRoot)_root).CreateRenderTarget(); + RenderTarget = ((IRenderRoot)_root).CreateRenderTarget(); } if (renderOverlay) @@ -191,8 +199,8 @@ namespace Avalonia.Rendering if (scene.Generation != _lastSceneId) { - context = _renderTarget.CreateDrawingContext(this); - _layers.Update(scene, context); + context = RenderTarget.CreateDrawingContext(this); + Layers.Update(scene, context); RenderToLayers(scene); @@ -208,13 +216,13 @@ namespace Avalonia.Rendering if (renderOverlay) { - context = context ?? _renderTarget.CreateDrawingContext(this); + context = context ?? RenderTarget.CreateDrawingContext(this); RenderOverlay(scene, context); RenderComposite(scene, context); } else if (composite) { - context = context ?? _renderTarget.CreateDrawingContext(this); + context = context ?? RenderTarget.CreateDrawingContext(this); RenderComposite(scene, context); } @@ -224,8 +232,8 @@ namespace Avalonia.Rendering catch (RenderTargetCorruptedException ex) { Logging.Logger.Information("Renderer", this, "Render target was corrupted. Exception: {0}", ex); - _renderTarget?.Dispose(); - _renderTarget = null; + RenderTarget?.Dispose(); + RenderTarget = null; } } @@ -235,9 +243,11 @@ namespace Avalonia.Rendering { clipBounds = node.ClipBounds.Intersect(clipBounds); - if (!clipBounds.IsEmpty) + if (!clipBounds.IsEmpty && node.Opacity > 0) { - node.BeginRender(context); + var isLayerRoot = node.Visual == layer; + + node.BeginRender(context, isLayerRoot); foreach (var operation in node.DrawOperations) { @@ -251,7 +261,7 @@ namespace Avalonia.Rendering Render(context, (VisualNode)child, layer, clipBounds); } - node.EndRender(context); + node.EndRender(context, isLayerRoot); } } } @@ -262,7 +272,7 @@ namespace Avalonia.Rendering { foreach (var layer in scene.Layers) { - var renderTarget = _layers[layer.LayerRoot].Bitmap; + var renderTarget = Layers[layer.LayerRoot].Bitmap; var node = (VisualNode)scene.FindNode(layer.LayerRoot); if (node != null) @@ -322,7 +332,7 @@ namespace Avalonia.Rendering foreach (var layer in scene.Layers) { - var bitmap = _layers[layer.LayerRoot].Bitmap; + var bitmap = Layers[layer.LayerRoot].Bitmap; var sourceRect = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight); if (layer.GeometryClip != null) @@ -353,7 +363,7 @@ namespace Avalonia.Rendering if (DrawFps) { - RenderFps(context, clientRect, true); + RenderFps(context, clientRect, scene.Layers.Count); } } @@ -442,7 +452,7 @@ namespace Avalonia.Rendering { var index = 0; - foreach (var layer in _layers) + foreach (var layer in Layers) { var fileName = Path.Combine(DebugFramesPath, $"frame-{id}-layer-{index++}.png"); layer.Bitmap.Save(fileName); diff --git a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs index 2d5a864089..84313f0906 100644 --- a/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs +++ b/src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs @@ -69,7 +69,7 @@ namespace Avalonia.Rendering if (DrawFps) { - RenderFps(context.PlatformImpl, _root.Bounds, true); + RenderFps(context.PlatformImpl, _root.Bounds, null); } } } diff --git a/src/Avalonia.Visuals/Rendering/RendererBase.cs b/src/Avalonia.Visuals/Rendering/RendererBase.cs index 707b31998a..eac362e997 100644 --- a/src/Avalonia.Visuals/Rendering/RendererBase.cs +++ b/src/Avalonia.Visuals/Rendering/RendererBase.cs @@ -22,15 +22,12 @@ namespace Avalonia.Rendering }; } - protected void RenderFps(IDrawingContextImpl context, Rect clientRect, bool incrementFrameCount) + protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount) { var now = _stopwatch.Elapsed; var elapsed = now - _lastFpsUpdate; - if (incrementFrameCount) - { - ++_framesThisSecond; - } + ++_framesThisSecond; if (elapsed.TotalSeconds > 1) { @@ -39,7 +36,15 @@ namespace Avalonia.Rendering _lastFpsUpdate = now; } - _fpsText.Text = string.Format("FPS: {0:000}", _fps); + if (layerCount.HasValue) + { + _fpsText.Text = string.Format("Layers: {0} FPS: {1:000}", layerCount, _fps); + } + else + { + _fpsText.Text = string.Format("FPS: {0:000}", _fps); + } + var size = _fpsText.Measure(); var rect = new Rect(clientRect.Right - size.Width, 0, size.Width, size.Height); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs index 0d2fc17b95..234cadbf31 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/IVisualNode.cs @@ -72,13 +72,15 @@ namespace Avalonia.Rendering.SceneGraph /// Sets up the drawing context for rendering the node's geometry. /// /// The drawing context. - void BeginRender(IDrawingContextImpl context); + /// Whether to skip pushing the control's opacity. + void BeginRender(IDrawingContextImpl context, bool skipOpacity); /// /// Resets the drawing context after rendering the node's geometry. /// /// The drawing context. - void EndRender(IDrawingContextImpl context); + /// Whether to skip popping the control's opacity. + void EndRender(IDrawingContextImpl context, bool skipOpacity); /// /// Hit test the geometry in this node. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs index 90ef78de37..8f4f487e08 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs @@ -167,7 +167,6 @@ namespace Avalonia.Rendering.SceneGraph using (context.PushPostTransform(m)) using (context.PushTransformContainer()) { - var startLayer = opacity < 1 || visual.OpacityMask != null; var clipBounds = bounds.TransformToAABB(contextImpl.Transform).Intersect(clip); forceRecurse = forceRecurse || @@ -179,9 +178,11 @@ namespace Avalonia.Rendering.SceneGraph node.ClipToBounds = clipToBounds; node.GeometryClip = visual.Clip?.PlatformImpl; node.Opacity = opacity; - node.OpacityMask = visual.OpacityMask; - if (startLayer) + // TODO: Check equality between node.OpacityMask and visual.OpacityMask before assigning. + node.OpacityMask = visual.OpacityMask?.ToImmutable(); + + if (ShouldStartLayer(visual)) { if (node.LayerRoot != visual) { @@ -192,7 +193,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); } @@ -366,6 +367,14 @@ namespace Avalonia.Rendering.SceneGraph } } + private static bool ShouldStartLayer(IVisual visual) + { + var o = visual as IAvaloniaObject; + return visual.VisualChildren.Count > 0 && + o != null && + o.IsAnimating(Visual.OpacityProperty); + } + private static IGeometryImpl CreateLayerGeometryClip(VisualNode node) { IGeometryImpl result = null; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index dd5740e4a9..6bea4d9bd6 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -22,6 +22,7 @@ namespace Avalonia.Rendering.SceneGraph private List _children; private List _drawOperations; private bool _drawOperationsCloned; + private Matrix transformRestore; /// /// Initializes a new instance of the class. @@ -218,8 +219,10 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void BeginRender(IDrawingContextImpl context) + public void BeginRender(IDrawingContextImpl context, bool skipOpacity) { + transformRestore = context.Transform; + if (ClipToBounds) { context.Transform = Matrix.Identity; @@ -228,24 +231,47 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = Transform; + if (Opacity != 1 && !skipOpacity) + { + context.PushOpacity(Opacity); + } + if (GeometryClip != null) { context.PushGeometryClip(GeometryClip); } + + if (OpacityMask != null) + { + context.PushOpacityMask(OpacityMask, ClipBounds); + } } /// - public void EndRender(IDrawingContextImpl context) + public void EndRender(IDrawingContextImpl context, bool skipOpacity) { + if (OpacityMask != null) + { + context.PopOpacityMask(); + } + if (GeometryClip != null) { context.PopGeometryClip(); } + if (Opacity != 1 && !skipOpacity) + { + context.PopOpacity(); + } + if (ClipToBounds) { + context.Transform = Matrix.Identity; context.PopClip(); } + + context.Transform = transformRestore; } private Rect CalculateBounds() diff --git a/src/Avalonia.Visuals/Visual.cs b/src/Avalonia.Visuals/Visual.cs index bc65d4f69f..3662fe50be 100644 --- a/src/Avalonia.Visuals/Visual.cs +++ b/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; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index a1db5c2915..0ce2a1a992 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -22,7 +22,7 @@ NETSTANDARD1_3;PCL;NETSTANDARD prompt 4 - bin\Release\Avalonia.Markup.Xaml.XML + bin\Release\Avalonia.Markup.Xaml.xml CS1591 @@ -91,6 +91,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoaderPortableXaml.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoaderPortableXaml.cs index 0419db5d0f..de2a79c54e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoaderPortableXaml.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoaderPortableXaml.cs @@ -127,7 +127,19 @@ namespace Avalonia.Markup.Xaml using (var stream = assetLocator.Open(uri, baseUri)) { - return Load(stream, rootInstance, uri); + try + { + return Load(stream, rootInstance, uri); + } + catch (Exception e) + { + var uriString = uri.ToString(); + if (!uri.IsAbsoluteUri) + { + uriString = new Uri(baseUri, uri).AbsoluteUri; + } + throw new XamlLoadException("Error loading xaml at " + uriString, e); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs index eb5f87ea2a..7f750144df 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Data/Binding.cs @@ -66,7 +66,7 @@ namespace Avalonia.Markup.Xaml.Data /// /// Gets or sets the binding path. /// - public string Path { get; set; } + public string Path { get; set; } = ""; /// /// Gets or sets the binding priority. @@ -93,53 +93,53 @@ namespace Avalonia.Markup.Xaml.Data bool enableDataValidation = false) { Contract.Requires(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(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(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(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(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; } } } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs b/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs index f77df6853b..825d3b8ba5 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Data/RelativeSource.cs +++ b/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. /// public RelativeSourceMode Mode { get; set; } + + public TreeType Tree { get; set; } = TreeType.Visual; } } \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs index 8984498393..c6705cbb4b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/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) { diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs index 4664947b8e..c5fe83977f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/TemplateBindingExtension.cs +++ b/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, }; } diff --git a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github index d50ae8335e..c066401445 160000 --- a/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github +++ b/src/Markup/Avalonia.Markup.Xaml/PortableXaml/portable.xaml.github @@ -1 +1 @@ -Subproject commit d50ae8335eb50d4b9606de6f5fa1cbbc78bfd72f +Subproject commit c0664014455392ac221a765e66f9837704339b6f diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlLoadException.cs b/src/Markup/Avalonia.Markup.Xaml/XamlLoadException.cs new file mode 100644 index 0000000000..e66b8ddf54 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlLoadException.cs @@ -0,0 +1,24 @@ +using System; +using System.Runtime.Serialization; + +namespace Avalonia.Markup.Xaml +{ + public class XamlLoadException: Exception + { + public XamlLoadException() + { + } + + protected XamlLoadException(SerializationInfo info, StreamingContext context): base(info, context) + { + } + + public XamlLoadException(string message): base(message) + { + } + + public XamlLoadException(string message, Exception innerException): base(message, innerException) + { + } + } +} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 57552f852c..a6d3c3336f 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -11,7 +11,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Markup.XML + bin\Debug\Avalonia.Markup.xml CS1591 @@ -21,7 +21,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Markup.XML + bin\Release\Avalonia.Markup.xml CS1591 true diff --git a/src/Markup/Avalonia.Markup/ControlLocator.cs b/src/Markup/Avalonia.Markup/ControlLocator.cs index de8415d6db..1a82c0a4fd 100644 --- a/src/Markup/Avalonia.Markup/ControlLocator.cs +++ b/src/Markup/Avalonia.Markup/ControlLocator.cs @@ -11,6 +11,21 @@ using Avalonia.VisualTree; namespace Avalonia.Markup { + /// + /// The type of tree via which to track a control. + /// + public enum TreeType + { + /// + /// The visual tree. + /// + Visual, + /// + /// The logical tree. + /// + Logical, + } + /// /// Locates controls relative to other controls. /// @@ -27,13 +42,13 @@ namespace Avalonia.Markup { var attached = Observable.FromEventPattern( 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( 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 /// /// The control relative from which the other control should be found. /// - /// The type of the ancestor to find. + /// The tree via which to track the control. /// /// The level of ancestor control to look for. Use 0 for the first ancestor of the /// requested type. /// - public static IObservable Track(IControl relativeTo, Type ancestorType, int ancestorLevel) + /// The type of the ancestor to find. + public static IObservable 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 TrackAttachmentToTree(IControl relativeTo, TreeType tree) + { + return tree == TreeType.Visual ? TrackAttachmentToVisualTree(relativeTo) : TrackAttachmentToLogicalTree(relativeTo); + } + + private static IObservable TrackAttachmentToVisualTree(IControl relativeTo) { var attached = Observable.FromEventPattern( x => relativeTo.AttachedToVisualTree += x, - x => relativeTo.DetachedFromVisualTree += x) + x => relativeTo.AttachedToVisualTree -= x) .Select(x => true) .StartWith(relativeTo.IsAttachedToVisualTree); var detached = Observable.FromEventPattern( 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 TrackAttachmentToLogicalTree(IControl relativeTo) + { + var attached = Observable.FromEventPattern( + x => relativeTo.AttachedToLogicalTree += x, + x => relativeTo.AttachedToLogicalTree -= x) + .Select(x => true) + .StartWith(relativeTo.IsAttachedToLogicalTree); + + var detached = Observable.FromEventPattern( + x => relativeTo.DetachedFromLogicalTree += x, + x => relativeTo.DetachedFromLogicalTree -= x) + .Select(x => false); + + var attachmentStatus = attached.Merge(detached); + return attachmentStatus; } } } diff --git a/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs index a824a38867..563b372c78 100644 --- a/src/Markup/Avalonia.Markup/Data/Parsers/ArgumentListParser.cs +++ b/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; diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index f727d033cc..f22722a0b5 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -450,7 +450,7 @@ namespace Avalonia.Skia { var match = _foregroundBrushes[bi]; - len = match.Key.EndIndex - index + 1; + len = match.Key.EndIndex - index; result = match.Value; if (len > 0 && len < length) @@ -641,7 +641,7 @@ namespace Avalonia.Skia Length = length; } - public int EndIndex => StartIndex + Length - 1; + public int EndIndex => StartIndex + Length; public int Length { get; private set; } diff --git a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs index 83bd4d2957..523cfeed46 100644 --- a/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs +++ b/src/Windows/Avalonia.Direct2D1/FramebufferShimRenderTarget.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Text; using Avalonia.Controls.Platform.Surfaces; using Avalonia.Direct2D1.Media; @@ -68,9 +69,9 @@ namespace Avalonia.Direct2D1 for (var y = 0; y < _target.Height; y++) { UnmanagedMethods.CopyMemory( - _target.Address + _target.RowBytes * y, - l.Data.DataPointer + l.Stride * y, - (uint) Math.Min(l.Stride, _target.RowBytes)); + (_target.Address + _target.RowBytes * y), + (l.Data.DataPointer + l.Stride * y), + (UIntPtr)Math.Min(l.Stride, _target.RowBytes)); } } Dispose(); diff --git a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs index 6a72923ce3..b1bfdcbfeb 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/DrawingContextImpl.cs +++ b/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(); } diff --git a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs index 9b99b4c40a..371dfcfc3e 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/Imaging/WicBitmapImpl.cs @@ -3,6 +3,7 @@ using System; using System.IO; +using System.Runtime.CompilerServices; using Avalonia.Win32.Interop; using SharpDX.WIC; using APixelFormat = Avalonia.Platform.PixelFormat; @@ -74,8 +75,10 @@ namespace Avalonia.Direct2D1.Media { for (var row = 0; row < height; row++) { - UnmanagedMethods.CopyMemory(new IntPtr(l.Data.DataPointer.ToInt64() + row * l.Stride), - new IntPtr(data.ToInt64() + row * stride), (uint) l.Data.Pitch); + UnmanagedMethods.CopyMemory( + (l.Data.DataPointer + row * l.Stride), + (data + row * stride), + (UIntPtr) l.Data.Pitch); } } } diff --git a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj index b2b42877b7..92d973238e 100644 --- a/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj +++ b/src/Windows/Avalonia.Win32/Avalonia.Win32.csproj @@ -22,7 +22,7 @@ DEBUG;TRACE prompt 4 - bin\Debug\Avalonia.Win32.XML + bin\Debug\Avalonia.Win32.xml true CS1591 @@ -33,7 +33,7 @@ TRACE prompt 4 - bin\Release\Avalonia.Win32.XML + bin\Release\Avalonia.Win32.xml true CS1591 true diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index fb4aefbf36..a0518cf92e 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -948,9 +948,9 @@ namespace Avalonia.Win32.Interop uint dwMaximumSizeLow, string lpName); - [DllImport("kernel32.dll", EntryPoint = "CopyMemory", SetLastError = false)] - public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count); - + [DllImport("msvcrt.dll", EntryPoint="memcpy", SetLastError = false, CallingConvention=CallingConvention.Cdecl)] + public static extern IntPtr CopyMemory(IntPtr dest, IntPtr src, UIntPtr count); + public enum MONITOR { MONITOR_DEFAULTTONULL = 0x00000000, diff --git a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj index 8692cdef42..f8ec52a6c7 100644 --- a/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj +++ b/tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.0 + net47;netcoreapp2.0 Library diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs index 4d6559a078..c75150ca6d 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Binding.cs @@ -2,22 +2,22 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Collections; -using System.Collections.Generic; +using System.ComponentModel; +using System.Reactive.Concurrency; using System.Reactive.Linq; using System.Reactive.Subjects; -using Microsoft.Reactive.Testing; +using System.Threading; +using System.Threading.Tasks; using Avalonia.Data; using Avalonia.Logging; -using Avalonia.UnitTests; -using Xunit; -using System.Threading.Tasks; +using Avalonia.Markup.Xaml.Data; using Avalonia.Platform; -using System.Threading; -using Moq; -using System.Reactive.Disposables; -using System.Reactive.Concurrency; using Avalonia.Threading; +using Avalonia.UnitTests; +using Avalonia.Diagnostics; +using Microsoft.Reactive.Testing; +using Moq; +using Xunit; namespace Avalonia.Base.UnitTests { @@ -363,7 +363,7 @@ namespace Avalonia.Base.UnitTests Assert.True(called); } } - + [Fact] public async Task Bind_With_Scheduler_Executes_On_Scheduler() { @@ -387,6 +387,77 @@ namespace Avalonia.Base.UnitTests } } + [Fact] + public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + var target = new Class1(); + + target.Bind(Class1.DoubleValueProperty, + new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel }); + + var child = new Class1(); + + child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty]; + + Assert.Equal(1, viewModel.SetterInvokedCount); + + // Issues #855 and #824 were causing a StackOverflowException at this point. + target.DoubleValue = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.DoubleValue); + Assert.Equal(expected, child.DoubleValue); + } + + [Fact] + public void IsAnimating_On_Property_With_No_Value_Returns_False() + { + var target = new Class1(); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Animation_Value_Returns_False() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "foo", BindingPriority.Animation); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Non_Animation_Binding_Returns_False() + { + var target = new Class1(); + var source = new Subject(); + + target.Bind(Class1.FooProperty, source, BindingPriority.LocalValue); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + + [Fact] + public void IsAnimating_On_Property_With_Animation_Binding_Returns_True() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.True(target.IsAnimating(Class1.FooProperty)); + } + /// /// Returns an observable that returns a single value but does not complete. /// @@ -405,6 +476,15 @@ namespace Avalonia.Base.UnitTests public static readonly StyledProperty QuxProperty = AvaloniaProperty.Register("Qux", 5.6); + + public static readonly StyledProperty DoubleValueProperty = + AvaloniaProperty.Register(nameof(DoubleValue)); + + public double DoubleValue + { + get { return GetValue(DoubleValueProperty); } + set { SetValue(DoubleValueProperty, value); } + } } private class Class2 : Class1 @@ -431,5 +511,40 @@ namespace Avalonia.Base.UnitTests return InstancedBinding.OneTime(_source); } } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private double _value; + + public event PropertyChangedEventHandler PropertyChanged; + + public double Value + { + get { return _value; } + set + { + if (_value != value) + { + SetterInvokedCount++; + if (SetterInvokedCount < MaxInvokedCount) + { + _value = (int)value; + if (_value > 75) _value = 75; + if (_value < 25) _value = 25; + } + else + { + _value = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + } } -} +} \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs index e9cb2bf450..f415d845ce 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_Direct.cs @@ -3,11 +3,18 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Reactive.Subjects; +using System.Threading; +using System.Threading.Tasks; using Avalonia; using Avalonia.Data; using Avalonia.Logging; +using Avalonia.Platform; +using Avalonia.Threading; +using Avalonia.Markup.Xaml.Data; using Avalonia.UnitTests; +using Moq; using Xunit; namespace Avalonia.Base.UnitTests @@ -208,7 +215,7 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => target.SetValue(Class1.BarProperty, "newvalue")); } @@ -217,7 +224,7 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => target.SetValue((AvaloniaProperty)Class1.BarProperty, "newvalue")); } @@ -227,7 +234,7 @@ namespace Avalonia.Base.UnitTests var target = new Class1(); var source = new Subject(); - Assert.Throws(() => + Assert.Throws(() => target.Bind(Class1.BarProperty, source)); } @@ -411,6 +418,28 @@ namespace Avalonia.Base.UnitTests Assert.True(called); } + [Fact] + public async Task Bind_Executes_On_UIThread() + { + var target = new Class1(); + var source = new Subject(); + var currentThreadId = Thread.CurrentThread.ManagedThreadId; + + var threadingInterfaceMock = new Mock(); + threadingInterfaceMock.SetupGet(mock => mock.CurrentThreadIsLoopThread) + .Returns(() => Thread.CurrentThread.ManagedThreadId == currentThreadId); + + var services = new TestServices( + threadingInterface: threadingInterfaceMock.Object); + + using (UnitTestApplication.Start(services)) + { + target.Bind(Class1.FooProperty, source); + + await Task.Run(() => source.OnNext("foobar")); + } + } + [Fact] public void AddOwner_Should_Inherit_DefaultBindingMode() { @@ -439,12 +468,46 @@ namespace Avalonia.Base.UnitTests Assert.Equal(BindingMode.OneWayToSource, bar.GetMetadata().DefaultBindingMode); } + [Fact] + public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + var target = new Class1(); + + target.Bind(Class1.DoubleValueProperty, new Binding("Value") + { + Mode = BindingMode.TwoWay, + Source = viewModel + }); + + var child = new Class1(); + + child[!!Class1.DoubleValueProperty] = target[!!Class1.DoubleValueProperty]; + + Assert.Equal(1, viewModel.SetterInvokedCount); + + // Issues #855 and #824 were causing a StackOverflowException at this point. + target.DoubleValue = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.DoubleValue); + Assert.Equal(expected, child.DoubleValue); + } + private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = AvaloniaProperty.RegisterDirect( - "Foo", - o => o.Foo, + "Foo", + o => o.Foo, (o, v) => o.Foo = v, unsetValue: "unset"); @@ -453,14 +516,21 @@ namespace Avalonia.Base.UnitTests public static readonly DirectProperty BazProperty = AvaloniaProperty.RegisterDirect( - "Bar", - o => o.Baz, - (o,v) => o.Baz = v, + "Bar", + o => o.Baz, + (o, v) => o.Baz = v, unsetValue: -1); + public static readonly DirectProperty DoubleValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(DoubleValue), + o => o.DoubleValue, + (o, v) => o.DoubleValue = v); + private string _foo = "initial"; private readonly string _bar = "bar"; private int _baz = 5; + private double _doubleValue; public string Foo { @@ -478,6 +548,12 @@ namespace Avalonia.Base.UnitTests get { return _baz; } set { SetAndRaise(BazProperty, ref _baz, value); } } + + public double DoubleValue + { + get { return _doubleValue; } + set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); } + } } private class Class2 : AvaloniaObject @@ -497,5 +573,40 @@ namespace Avalonia.Base.UnitTests set { SetAndRaise(FooProperty, ref _foo, value); } } } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private double _value; + + public event PropertyChangedEventHandler PropertyChanged; + + public double Value + { + get { return _value; } + set + { + if (_value != value) + { + SetterInvokedCount++; + if (SetterInvokedCount < MaxInvokedCount) + { + _value = (int)value; + if (_value > 75) _value = 75; + if (_value < 25) _value = 25; + } + else + { + _value = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + } } -} +} \ No newline at end of file diff --git a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs b/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs index 587816b07b..fd731455d8 100644 --- a/tests/Avalonia.Base.UnitTests/Collections/AvaloniaListTests.cs +++ b/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(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() { diff --git a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs index 3a37585dc0..84ff492512 100644 --- a/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs +++ b/tests/Avalonia.Base.UnitTests/DirectPropertyTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Reactive.Subjects; using Avalonia.Data; using Xunit; @@ -70,6 +71,17 @@ namespace Avalonia.Base.UnitTests Assert.Same(p1.Initialized, p2.Initialized); } + [Fact] + public void IsAnimating_On_DirectProperty_With_Binding_Returns_False() + { + var target = new Class1(); + var source = new BehaviorSubject("foo"); + + target.Bind(Class1.FooProperty, source, BindingPriority.Animation); + + Assert.False(target.IsAnimating(Class1.FooProperty)); + } + private class Class1 : AvaloniaObject { public static readonly DirectProperty FooProperty = diff --git a/tests/Avalonia.Benchmarks/App.config b/tests/Avalonia.Benchmarks/App.config index 121e469348..425e308058 100644 --- a/tests/Avalonia.Benchmarks/App.config +++ b/tests/Avalonia.Benchmarks/App.config @@ -1,13 +1,13 @@ - + - + - - + + diff --git a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj index 1d987e2238..d40dc596a5 100644 --- a/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj +++ b/tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj @@ -9,7 +9,7 @@ Properties Avalonia.Benchmarks Avalonia.Benchmarks - v4.6.1 + v4.7 512 true @@ -49,6 +49,7 @@ + diff --git a/tests/Avalonia.Benchmarks/Base/Properties.cs b/tests/Avalonia.Benchmarks/Base/Properties.cs new file mode 100644 index 0000000000..0a020961d5 --- /dev/null +++ b/tests/Avalonia.Benchmarks/Base/Properties.cs @@ -0,0 +1,43 @@ +using System; +using System.Reactive.Subjects; +using BenchmarkDotNet.Attributes; + +namespace Avalonia.Benchmarks.Base +{ + [MemoryDiagnoser] + public class AvaloniaObjectBenchmark + { + private Class1 target = new Class1(); + private Subject intBinding = new Subject(); + + public AvaloniaObjectBenchmark() + { + target.SetValue(Class1.IntProperty, 123); + } + + [Benchmark] + public void ClearAndSetIntProperty() + { + target.ClearValue(Class1.IntProperty); + target.SetValue(Class1.IntProperty, 123); + } + + [Benchmark] + public void BindIntProperty() + { + using (target.Bind(Class1.IntProperty, intBinding)) + { + for (var i = 0; i < 100; ++i) + { + intBinding.OnNext(i); + } + } + } + + class Class1 : AvaloniaObject + { + public static readonly AvaloniaProperty IntProperty = + AvaloniaProperty.Register("Int"); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs index 867f740a3a..60c53d126c 100644 --- a/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AppBuilderTests.cs @@ -65,6 +65,7 @@ namespace Avalonia.Controls.UnitTests { ResetModuleLoadStates(); AppBuilder.Configure() + .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }) .UseAvaloniaModules() @@ -81,6 +82,7 @@ namespace Avalonia.Controls.UnitTests { ResetModuleLoadStates(); var builder = AppBuilder.Configure() + .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }, "Direct2D1"); builder.UseAvaloniaModules().SetupWithoutStarting(); @@ -90,6 +92,7 @@ namespace Avalonia.Controls.UnitTests ResetModuleLoadStates(); builder = AppBuilder.Configure() + .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }, "Skia"); builder.UseAvaloniaModules().SetupWithoutStarting(); @@ -99,13 +102,14 @@ namespace Avalonia.Controls.UnitTests } } - [Fact (Skip = "We don't have rendering modules with dependencies right now")] + [Fact] public void LoadsRenderingModuleWithoutDependenciesWhenNoModuleMatches() { using (AvaloniaLocator.EnterScope()) { ResetModuleLoadStates(); var builder = AppBuilder.Configure() + .IgnoreSetupCheck() .UseWindowingSubsystem(() => { }) .UseRenderingSubsystem(() => { }, "TBD"); builder.UseAvaloniaModules().SetupWithoutStarting(); diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 8b2f5093cf..a63898486b 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.0 + net47;netcoreapp2.0 Library diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index 5f86b9878d..653ab17b63 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -1,6 +1,7 @@ // 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; using System.Collections.Specialized; using System.Linq; using Moq; @@ -11,6 +12,9 @@ using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Data; +using System.Collections.Generic; namespace Avalonia.Controls.UnitTests { @@ -273,6 +277,60 @@ namespace Avalonia.Controls.UnitTests Assert.Null(target.Presenter.Child.DataContext); } + [Fact] + public void Binding_ContentTemplate_After_Content_Does_Not_Leave_Orpaned_TextBlock() + { + // Test for #1271. + var children = new List(); + var presenter = new ContentPresenter(); + + // The content and then the content template property need to be bound with delayed bindings + // as they are in Avalonia.Markup.Xaml. + DelayedBinding.Add(presenter, ContentPresenter.ContentProperty, new Binding("Content") + { + Priority = BindingPriority.TemplatedParent, + RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), + }); + + DelayedBinding.Add(presenter, ContentPresenter.ContentTemplateProperty, new Binding("ContentTemplate") + { + Priority = BindingPriority.TemplatedParent, + RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), + }); + + presenter.GetObservable(ContentPresenter.ChildProperty).Subscribe(children.Add); + + var target = new ContentControl + { + Template = new FuncControlTemplate(_ => presenter), + ContentTemplate = new FuncDataTemplate(x => new Canvas()), + Content = "foo", + }; + + // The control must be rooted. + var root = new TestRoot + { + Child = target, + }; + + target.ApplyTemplate(); + + // When the template is applied, the Content property is bound before the ContentTemplate + // property, causing a TextBlock to be created by the default template before ContentTemplate + // is bound. + Assert.Collection( + children, + x => Assert.Null(x), + x => Assert.IsType(x), + x => Assert.IsType(x)); + + var textBlock = (TextBlock)children[1]; + + // The leak in #1271 was caused by the TextBlock's logical parent not being cleared when + // it is replaced by the Canvas. + Assert.Null(textBlock.GetLogicalParent()); + } + private FuncControlTemplate GetTemplate() { return new FuncControlTemplate(parent => diff --git a/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs index 6a6cb48001..9ee6e3e456 100644 --- a/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs +++ b/tests/Avalonia.Controls.UnitTests/ControlTests_Resources.cs @@ -168,7 +168,7 @@ namespace Avalonia.Controls.UnitTests target.Resources.Add("foo", "bar"); Assert.True(raisedOnTarget); - Assert.False(raisedOnPresenter); + Assert.True(raisedOnPresenter); Assert.True(raisedOnChild); } @@ -204,6 +204,22 @@ namespace Avalonia.Controls.UnitTests Assert.True(raised); } + [Fact] + public void Setting_Logical_Parent_Subscribes_To_Parents_ResourceChanged_Event() + { + var parent = new ContentControl(); + var child = new Border(); + + ((ISetLogicalParent)child).SetParent(parent); + var raisedOnChild = false; + + child.ResourcesChanged += (_, __) => raisedOnChild = true; + + parent.Resources.Add("foo", "bar"); + + Assert.True(raisedOnChild); + } + private IControlTemplate ContentControlTemplate() { return new FuncControlTemplate(x => diff --git a/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs b/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs index db4c3c0eca..3de67839a7 100644 --- a/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DockPanelTests.cs @@ -12,7 +12,7 @@ namespace Avalonia.Controls.UnitTests { var target = new DockPanel { - Children = new Controls + Children = { new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top }, new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom }, @@ -38,7 +38,7 @@ namespace Avalonia.Controls.UnitTests { var target = new DockPanel { - Children = new Controls + Children = { new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left }, new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right }, diff --git a/tests/Avalonia.Controls.UnitTests/DropDownTests.cs b/tests/Avalonia.Controls.UnitTests/DropDownTests.cs index b5de8c67fa..29e60a8f83 100644 --- a/tests/Avalonia.Controls.UnitTests/DropDownTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DropDownTests.cs @@ -89,7 +89,7 @@ namespace Avalonia.Controls.UnitTests return new Panel { Name = "container", - Children = new Controls + Children = { new ContentControl { diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index 5d622a5fc1..a1ba608ec6 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -22,14 +22,14 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid() { - RowDefinitions = new RowDefinitions("*,Auto,*"), - ColumnDefinitions = new ColumnDefinitions("*,*"), - Children = new Controls() - { - new Border { [Grid.RowProperty] = 0 }, - new GridSplitter { [Grid.RowProperty] = 1, Name = "splitter" }, - new Border { [Grid.RowProperty] = 2 } - } + RowDefinitions = new RowDefinitions("*,Auto,*"), + ColumnDefinitions = new ColumnDefinitions("*,*"), + Children = + { + new Border { [Grid.RowProperty] = 0 }, + new GridSplitter { [Grid.RowProperty] = 1, Name = "splitter" }, + new Border { [Grid.RowProperty] = 2 } + } }; var root = new TestRoot { Child = grid }; @@ -43,14 +43,14 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid() { - ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = new Controls() - { - new Border { [Grid.ColumnProperty] = 0 }, - new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, - new Border { [Grid.ColumnProperty] = 2 }, - } + ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, + new Border { [Grid.ColumnProperty] = 2 }, + } }; var root = new TestRoot { Child = grid }; @@ -64,14 +64,14 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid() { - ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), - RowDefinitions = new RowDefinitions("Auto,Auto"), - Children = new Controls() - { - new Border { [Grid.ColumnProperty] = 0 }, - new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, - new Border { [Grid.ColumnProperty] = 2 }, - } + ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), + RowDefinitions = new RowDefinitions("Auto,Auto"), + Children = + { + new Border { [Grid.ColumnProperty] = 0 }, + new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, + new Border { [Grid.ColumnProperty] = 2 }, + } }; var root = new TestRoot { Child = grid }; @@ -99,11 +99,11 @@ namespace Avalonia.Controls.UnitTests var grid = new Grid() { - RowDefinitions = rowDefinitions, - Children = new Controls() - { - control1, splitter, control2 - } + RowDefinitions = rowDefinitions, + Children = + { + control1, splitter, control2 + } }; var root = new TestRoot { Child = grid }; @@ -131,14 +131,14 @@ namespace Avalonia.Controls.UnitTests { var grid = new Grid() { - ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), - RowDefinitions = new RowDefinitions("*,*"), - Children = new Controls() - { - new GridSplitter { [Grid.ColumnProperty] = 0, Name = "splitter" }, - new Border { [Grid.ColumnProperty] = 1 }, - new Border { [Grid.ColumnProperty] = 2 }, - } + ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), + RowDefinitions = new RowDefinitions("*,*"), + Children = + { + new GridSplitter { [Grid.ColumnProperty] = 0, Name = "splitter" }, + new Border { [Grid.ColumnProperty] = 1 }, + new Border { [Grid.ColumnProperty] = 2 }, + } }; var root = new TestRoot { Child = grid }; @@ -171,11 +171,11 @@ namespace Avalonia.Controls.UnitTests var grid = new Grid() { - ColumnDefinitions = columnDefinitions, - Children = new Controls() - { - control1, splitter, control2 - } + ColumnDefinitions = columnDefinitions, + Children = + { + control1, splitter, control2 + } }; var root = new TestRoot { Child = grid }; diff --git a/tests/Avalonia.Controls.UnitTests/GridTests.cs b/tests/Avalonia.Controls.UnitTests/GridTests.cs index a10bb38322..c5aea6501f 100644 --- a/tests/Avalonia.Controls.UnitTests/GridTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridTests.cs @@ -24,7 +24,7 @@ namespace Avalonia.Controls.UnitTests new RowDefinition(GridLength.Auto), new RowDefinition(GridLength.Auto), }, - Children = new Controls + Children = { new Border { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index c7992fe80f..fee4994ee3 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -1,11 +1,15 @@ // 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 System.ComponentModel; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Templates; +using Avalonia.Data; using Avalonia.Input; using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml.Data; using Avalonia.Styling; using Avalonia.VisualTree; using Xunit; @@ -199,6 +203,71 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, target.SelectedIndex); } + [Fact] + public void SelectedItem_Should_Not_Cause_StackOverflow() + { + var viewModel = new TestStackOverflowViewModel() + { + Items = new List { "foo", "bar", "baz" } + }; + + var target = new ListBox + { + Template = new FuncControlTemplate(CreateListBoxTemplate), + DataContext = viewModel, + Items = viewModel.Items + }; + + target.Bind(ListBox.SelectedItemProperty, + new Binding("SelectedItem") { Mode = BindingMode.TwoWay }); + + Assert.Equal(0, viewModel.SetterInvokedCount); + + // In Issue #855, a Stackoverflow occured here. + target.SelectedItem = viewModel.Items[2]; + + Assert.Equal(viewModel.Items[1], target.SelectedItem); + Assert.Equal(1, viewModel.SetterInvokedCount); + } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public List Items { get; set; } + + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private string _selectedItem; + + public event PropertyChangedEventHandler PropertyChanged; + + public string SelectedItem + { + get { return _selectedItem; } + set + { + if (_selectedItem != value) + { + SetterInvokedCount++; + + int index = Items.IndexOf(value); + + if (MaxInvokedCount > SetterInvokedCount && index > 0) + { + _selectedItem = Items[index - 1]; + } + else + { + _selectedItem = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedItem))); + } + } + } + } + private Control CreateListBoxTemplate(ITemplatedControl parent) { return new ScrollViewer @@ -237,4 +306,4 @@ namespace Avalonia.Controls.UnitTests target.Presenter.ApplyTemplate(); } } -} +} \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/PanelTests.cs b/tests/Avalonia.Controls.UnitTests/PanelTests.cs index fb1ae3ba1a..ed239120d6 100644 --- a/tests/Avalonia.Controls.UnitTests/PanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/PanelTests.cs @@ -2,8 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Linq; -using Avalonia.Collections; using Avalonia.LogicalTree; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests @@ -18,20 +18,9 @@ namespace Avalonia.Controls.UnitTests panel.Children.Add(child); - Assert.Equal(child.Parent, panel); - Assert.Equal(child.GetLogicalParent(), panel); - } - - [Fact] - public void Setting_Controls_Should_Set_Child_Controls_Parent() - { - var panel = new Panel(); - var child = new Control(); - - panel.Children = new Controls { 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 +34,7 @@ namespace Avalonia.Controls.UnitTests Assert.Null(child.Parent); Assert.Null(child.GetLogicalParent()); + Assert.Null(child.GetVisualParent()); } [Fact] @@ -60,62 +50,70 @@ 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 Resetting_Panel_Children_Should_Clear_Child_Controls_Parent() + public void Replacing_Panel_Children_Should_Clear_And_Set_Control_Parent() { var panel = new Panel(); var child1 = new Control(); var child2 = new Control(); panel.Children.Add(child1); - panel.Children.Add(child2); - panel.Children = new Controls(); + panel.Children[0] = child2; Assert.Null(child1.Parent); Assert.Null(child1.GetLogicalParent()); - Assert.Null(child2.Parent); - Assert.Null(child2.GetLogicalParent()); + Assert.Null(child1.GetVisualParent()); + Assert.Same(child2.Parent, panel); + Assert.Same(child2.GetLogicalParent(), panel); + Assert.Same(child2.GetVisualParent(), panel); } [Fact] - public void Setting_Children_Should_Make_Controls_Appear_In_Panel_Children() + public void Child_Control_Should_Appear_In_Panel_Logical_And_Visual_Children() { var panel = new Panel(); var child = new Control(); - panel.Children = new Controls { child }; + panel.Children.Add(child); Assert.Equal(new[] { child }, panel.Children); Assert.Equal(new[] { child }, panel.GetLogicalChildren()); + Assert.Equal(new[] { child }, panel.GetVisualChildren()); } [Fact] - public void Child_Control_Should_Appear_In_Panel_Children() + public void Removing_Child_Control_Should_Remove_From_Panel_Logical_And_Visual_Children() { var panel = new Panel(); var child = new Control(); panel.Children.Add(child); + panel.Children.Remove(child); - Assert.Equal(new[] { child }, panel.Children); - Assert.Equal(new[] { child }, panel.GetLogicalChildren()); + Assert.Equal(new Control[0], panel.Children); + Assert.Empty(panel.GetLogicalChildren()); + Assert.Empty(panel.GetVisualChildren()); } [Fact] - public void Removing_Child_Control_Should_Remove_From_Panel_Children() + public void Moving_Panel_Children_Should_Reoder_Logical_And_Visual_Children() { var panel = new Panel(); - var child = new Control(); + var child1 = new Control(); + var child2 = new Control(); - panel.Children.Add(child); - panel.Children.Remove(child); + panel.Children.Add(child1); + panel.Children.Add(child2); + panel.Children.Move(1, 0); - Assert.Equal(new Control[0], panel.Children); - Assert.Equal(new ILogical[0], panel.GetLogicalChildren()); + Assert.Equal(new[] { child2, child1 }, panel.GetLogicalChildren()); + Assert.Equal(new[] { child2, child1 }, panel.GetVisualChildren()); } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 64344a1584..d4b5d01a6b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -2,11 +2,14 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; +using Avalonia.Styling; using Avalonia.UnitTests; +using Avalonia.VisualTree; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -35,6 +38,25 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void PopupRoot_StylingParent_Is_Popup() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new TemplatedControlWithPopup + { + PopupContent = new Canvas(), + }; + + var root = new TestRoot { Child = target }; + + target.ApplyTemplate(); + target.Popup.Open(); + + Assert.Equal(target.Popup, ((IStyleHost)target.Popup.PopupRoot).StylingParent); + } + } + [Fact] public void Attaching_PopupRoot_To_Parent_Logical_Tree_Raises_DetachedFromLogicalTree_And_AttachedToLogicalTree() { @@ -90,14 +112,33 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Clearing_Content_Of_Popup_In_ControlTemplate_Doesnt_Crash() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new TemplatedControlWithPopup + { + PopupContent = new Canvas(), + }; + + var root = new TestRoot { Child = target }; + + target.ApplyTemplate(); + target.Popup.Open(); + target.PopupContent = null; + } + } + private PopupRoot CreateTarget() { var result = new PopupRoot { - Template = new FuncControlTemplate(_ => + Template = new FuncControlTemplate(parent => new ContentPresenter { Name = "PART_ContentPresenter", + [!ContentPresenter.ContentProperty] = parent[!PopupRoot.ContentProperty], }), }; @@ -105,5 +146,33 @@ namespace Avalonia.Controls.UnitTests.Primitives return result; } + + private class TemplatedControlWithPopup : TemplatedControl + { + public static readonly AvaloniaProperty PopupContentProperty = + AvaloniaProperty.Register(nameof(PopupContent)); + + public TemplatedControlWithPopup() + { + Template = new FuncControlTemplate(parent => + new Popup + { + [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty], + }); + } + + public Popup Popup { get; private set; } + + public Control PopupContent + { + get => GetValue(PopupContentProperty); + set => SetValue(PopupContentProperty, value); + } + + protected override void OnTemplateApplied(TemplateAppliedEventArgs e) + { + Popup = (Popup)this.GetVisualChildren().Single(); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 06b3202a83..84ea717bab 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -15,6 +15,7 @@ using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; +using Avalonia.Input; namespace Avalonia.Controls.UnitTests.Primitives { @@ -189,9 +190,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { + var window = new Window(); var target = new Popup(); var child = new Control(); + window.Content = target; target.Open(); Assert.Single(target.PopupRoot.GetVisualChildren()); @@ -214,7 +217,8 @@ namespace Avalonia.Controls.UnitTests.Primitives { Content = new Border(), Template = new FuncControlTemplate(PopupContentControlTemplate), - } + }, + StylingParent = AvaloniaLocator.Current.GetService() }; target.ApplyTemplate(); @@ -306,7 +310,8 @@ namespace Avalonia.Controls.UnitTests.Primitives .Bind().ToFunc(() => globalStyles.Object) .Bind().ToConstant(new WindowingPlatformMock()) .Bind().ToTransient() - .Bind().ToFunc(() => renderInterface.Object); + .Bind().ToFunc(() => renderInterface.Object) + .Bind().ToConstant(new InputManager()); return result; } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index d3ed077cbf..2dfb30a9f0 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -2,7 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.ComponentModel; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Data; +using Avalonia.Markup.Xaml.Data; +using Avalonia.Styling; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -87,8 +92,111 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Throws(() => target.Value = double.NegativeInfinity); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SetValue_Should_Not_Cause_StackOverflow(bool useXamlBinding) + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + Track track = null; + + var target = new TestRange() + { + Template = new FuncControlTemplate(c => + { + track = new Track() + { + Width = 100, + Orientation = Orientation.Horizontal, + [~~Track.MinimumProperty] = c[~~RangeBase.MinimumProperty], + [~~Track.MaximumProperty] = c[~~RangeBase.MaximumProperty], + + Name = "PART_Track", + Thumb = new Thumb() + }; + + if (useXamlBinding) + { + track.Bind(Track.ValueProperty, new Binding("Value") + { + Mode = BindingMode.TwoWay, + Source = c, + Priority = BindingPriority.Style + }); + } + else + { + track[~~Track.ValueProperty] = c[~~RangeBase.ValueProperty]; + } + + return track; + }), + Minimum = 0, + Maximum = 100, + DataContext = viewModel + }; + + target.Bind(TestRange.ValueProperty, new Binding("Value") { Mode = BindingMode.TwoWay }); + + target.ApplyTemplate(); + track.Measure(new Size(100, 0)); + track.Arrange(new Rect(0, 0, 100, 0)); + + Assert.Equal(1, viewModel.SetterInvokedCount); + + // Issues #855 and #824 were causing a StackOverflowException at this point. + target.Value = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.Value); + Assert.Equal(expected, track.Value); + } + private class TestRange : RangeBase { } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private double _value; + + public event PropertyChangedEventHandler PropertyChanged; + + public double Value + { + get { return _value; } + set + { + if (_value != value) + { + SetterInvokedCount++; + if (SetterInvokedCount < MaxInvokedCount) + { + _value = (int)value; + if (_value > 75) _value = 75; + if (_value < 25) _value = 25; + } + else + { + _value = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + } } } \ No newline at end of file diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index be3c34ac2e..cd71717619 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -67,7 +67,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new Panel { - Children = new Controls + Children = { new TextBlock(), new Border(), @@ -101,7 +101,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new Panel { - Children = new Controls + Children = { new TextBlock(), new Border(), @@ -124,7 +124,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new Panel { - Children = new Controls + Children = { new TextBlock(), new Border(), @@ -189,7 +189,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { return new StackPanel { - Children = new Controls + Children = { new TextBlock { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs index f07a5f5095..5396a43f3a 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TrackTests.cs @@ -140,5 +140,31 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Same(thumb.Parent, target); Assert.Equal(new[] { thumb }, ((ILogical)target).LogicalChildren); } + + [Fact] + public void Should_Not_Pass_Invalid_Arrange_Rect() + { + var thumb = new Thumb { Width = 100.873106060606 }; + var increaseButton = new Button { Width = 10 }; + var decreaseButton = new Button { Width = 10 }; + + var target = new Track + { + Height = 12, + Thumb = thumb, + IncreaseButton = increaseButton, + DecreaseButton = decreaseButton, + Orientation = Orientation.Horizontal, + Minimum = 0, + Maximum = 287, + Value = 287, + ViewportSize = 241, + }; + + target.Measure(Size.Infinity); + + // #1297 was occuring here. + target.Arrange(new Rect(0, 0, 221, 12)); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 8f815a85c9..68dcfcb770 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -50,7 +50,7 @@ namespace Avalonia.Controls.UnitTests new RowDefinition(1, GridUnitType.Star), new RowDefinition(GridLength.Auto), }, - Children = new Controls + Children = { new ScrollContentPresenter { diff --git a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs index 56412d732b..ba80cb779a 100644 --- a/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/StackPanelTests.cs @@ -13,7 +13,7 @@ namespace Avalonia.Controls.UnitTests { var target = new StackPanel { - Children = new Controls + Children = { new Border { Height = 20, Width = 120 }, new Border { Height = 30 }, @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests var target = new StackPanel { Orientation = Orientation.Horizontal, - Children = new Controls + Children = { new Border { Width = 20, Height = 120 }, new Border { Width = 30 }, @@ -59,7 +59,7 @@ namespace Avalonia.Controls.UnitTests var target = new StackPanel { Gap = 10, - Children = new Controls + Children = { new Border { Height = 20, Width = 120 }, new Border { Height = 30 }, @@ -83,7 +83,7 @@ namespace Avalonia.Controls.UnitTests { Gap = 10, Orientation = Orientation.Horizontal, - Children = new Controls + Children = { new Border { Width = 20, Height = 120 }, new Border { Width = 30 }, @@ -106,7 +106,7 @@ namespace Avalonia.Controls.UnitTests var target = new StackPanel { Height = 60, - Children = new Controls + Children = { new Border { Height = 20, Width = 120 }, new Border { Height = 30 }, @@ -130,7 +130,7 @@ namespace Avalonia.Controls.UnitTests { Width = 60, Orientation = Orientation.Horizontal, - Children = new Controls + Children = { new Border { Width = 20, Height = 120 }, new Border { Width = 30 }, diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index 638d773cd0..67b224d6a0 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -272,7 +272,7 @@ namespace Avalonia.Controls.UnitTests { return new StackPanel { - Children = new Controls + Children = { new TabStrip { diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 26fc2a2461..b091f6826e 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -202,6 +202,22 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_SelectionStart_To_SelectionEnd_Sets_CaretPosition_To_SelectionStart() + { + using (UnitTestApplication.Start(Services)) + { + var textBox = new TextBox + { + Text = "0123456789" + }; + + textBox.SelectionStart = 2; + textBox.SelectionEnd = 2; + Assert.Equal(2, textBox.CaretIndex); + } + } + [Fact] public void Setting_Text_Updates_CaretPosition() { diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 17eccc96bc..625d9eb26e 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -432,7 +432,7 @@ namespace Avalonia.Controls.UnitTests { return new FuncControlTemplate(parent => new Panel { - Children = new Controls + Children = { new ContentPresenter { diff --git a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs index 1a46e24b16..cd35627064 100644 --- a/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WrapPanelTests.cs @@ -12,12 +12,12 @@ namespace Avalonia.Controls.UnitTests { var target = new WrapPanel() { - Width = 100, - Children = new Controls - { - new Border { Height = 50, Width = 100 }, - new Border { Height = 50, Width = 100 }, - } + Width = 100, + Children = + { + new Border { Height = 50, Width = 100 }, + new Border { Height = 50, Width = 100 }, + } }; target.Measure(Size.Infinity); @@ -33,12 +33,12 @@ namespace Avalonia.Controls.UnitTests { var target = new WrapPanel() { - Width = 200, - Children = new Controls - { - new Border { Height = 50, Width = 100 }, - new Border { Height = 50, Width = 100 }, - } + Width = 200, + Children = + { + new Border { Height = 50, Width = 100 }, + new Border { Height = 50, Width = 100 }, + } }; target.Measure(Size.Infinity); @@ -54,13 +54,13 @@ namespace Avalonia.Controls.UnitTests { var target = new WrapPanel() { - Orientation = Orientation.Vertical, - Height = 120, - Children = new Controls - { - new Border { Height = 50, Width = 100 }, - new Border { Height = 50, Width = 100 }, - } + Orientation = Orientation.Vertical, + Height = 120, + Children = + { + new Border { Height = 50, Width = 100 }, + new Border { Height = 50, Width = 100 }, + } }; target.Measure(Size.Infinity); @@ -76,13 +76,13 @@ namespace Avalonia.Controls.UnitTests { var target = new WrapPanel() { - Orientation = Orientation.Vertical, - Height = 60, - Children = new Controls - { - new Border { Height = 50, Width = 100 }, - new Border { Height = 50, Width = 100 }, - } + Orientation = Orientation.Vertical, + Height = 60, + Children = + { + new Border { Height = 50, Width = 100 }, + new Border { Height = 50, Width = 100 }, + } }; target.Measure(Size.Infinity); diff --git a/tests/Avalonia.DesignerSupport.TestApp/App.config b/tests/Avalonia.DesignerSupport.TestApp/App.config index 4fe7131d2a..baef7524af 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/App.config +++ b/tests/Avalonia.DesignerSupport.TestApp/App.config @@ -1,18 +1,18 @@ - + - + - - + + - - + + - \ No newline at end of file + diff --git a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj index 7945915e8c..fbbd5f0b28 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj +++ b/tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj @@ -9,16 +9,17 @@ Properties Avalonia.DesignerSupport.TestApp Avalonia.DesignerSupport.TestApp - v4.6.1 + v4.7 512 true + + ..\..\artifacts\designer-tests\ AnyCPU true full false - ..\..\artifacts\tests\ DEBUG;TRACE prompt 4 @@ -27,7 +28,6 @@ AnyCPU pdbonly true - ..\..\artifacts\tests\ TRACE prompt 4 @@ -62,6 +62,7 @@ True Resources.resx + True Designer diff --git a/tests/Avalonia.DesignerSupport.TestApp/Properties/Resources.Designer.cs b/tests/Avalonia.DesignerSupport.TestApp/Properties/Resources.Designer.cs index a251d20120..28db9d4dcf 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Properties/Resources.Designer.cs +++ b/tests/Avalonia.DesignerSupport.TestApp/Properties/Resources.Designer.cs @@ -8,10 +8,10 @@ // //------------------------------------------------------------------------------ -namespace Avalonia.DesignerSupport.TestApp.Properties -{ - - +namespace Avalonia.DesignerSupport.TestApp.Properties { + using System; + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// @@ -19,51 +19,43 @@ namespace Avalonia.DesignerSupport.TestApp.Properties // class via a tool like ResGen or Visual Studio. // To add or remove a member, edit your .ResX file then rerun ResGen // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources - { - + internal class Resources { + private static global::System.Resources.ResourceManager resourceMan; - + private static global::System.Globalization.CultureInfo resourceCulture; - + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() - { + internal Resources() { } - + /// /// Returns the cached ResourceManager instance used by this class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager - { - get - { - if ((resourceMan == null)) - { + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Avalonia.DesignerSupport.TestApp.Properties.Resources", typeof(Resources).Assembly); resourceMan = temp; } return resourceMan; } } - + /// /// Overrides the current thread's CurrentUICulture property for all /// resource lookups using this strongly typed resource class. /// [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture - { - get - { + internal static global::System.Globalization.CultureInfo Culture { + get { return resourceCulture; } - set - { + set { resourceCulture = value; } } diff --git a/tests/Avalonia.DesignerSupport.TestApp/Properties/Settings.Designer.cs b/tests/Avalonia.DesignerSupport.TestApp/Properties/Settings.Designer.cs index eaf8ac263a..15a90aaf18 100644 --- a/tests/Avalonia.DesignerSupport.TestApp/Properties/Settings.Designer.cs +++ b/tests/Avalonia.DesignerSupport.TestApp/Properties/Settings.Designer.cs @@ -8,21 +8,17 @@ // //------------------------------------------------------------------------------ -namespace Avalonia.DesignerSupport.TestApp.Properties -{ - - +namespace Avalonia.DesignerSupport.TestApp.Properties { + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] - internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase - { - + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "15.5.0.0")] + internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { + private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); - - public static Settings Default - { - get - { + + public static Settings Default { + get { return defaultInstance; } } diff --git a/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj b/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj index 477d44b99d..4ce24e1d4c 100644 --- a/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj +++ b/tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj @@ -9,14 +9,15 @@ Properties Avalonia.DesignerSupport.Tests Avalonia.DesignerSupport.Tests - v4.6.1 + v4.7 512 + + ..\..\artifacts\designer-tests\ true full false - ..\..\artifacts\tests\ DEBUG;TRACE prompt 4 @@ -24,7 +25,6 @@ pdbonly true - ..\..\artifacts\tests\ TRACE prompt 4 diff --git a/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj b/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj index 4b33d14243..f6b5d11af1 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj +++ b/tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj @@ -1,95 +1,22 @@ - - - + - Debug - AnyCPU - {EFB11458-9CDF-41C0-BE4F-44AF45A4CAB8} - Library - Properties - Avalonia.Direct2D1.UnitTests - Avalonia.Direct2D1.UnitTests - v4.6.1 - 512 - + net47;netcoreapp2.0 - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - - - - - - - - - - {D211E587-D8BC-45B9-95A4-F297C8FA5200} - Avalonia.Animation - - - {B09B78D8-9B26-48B0-9149-D64A2F120F3F} - Avalonia.Base - - - {D2221C82-4A25-4583-9B43-D791E3F6820C} - Avalonia.Controls - - - {62024B2D-53EB-4638-B26B-85EEAA54866E} - Avalonia.Input - - - {6B0ED19D-A08B-461C-A9D9-A9EE40B0C06B} - Avalonia.Interactivity - - - {42472427-4774-4C81-8AFF-9F27B8E31721} - Avalonia.Layout - - - {EB582467-6ABB-43A1-B052-E981BA910E3A} - Avalonia.Visuals - - - {F1BAA01A-F176-4C6A-B39D-5B40BB1B148F} - Avalonia.Styling - - - {3E908F67-5543-4879-A1DC-08EACE79B3CD} - Avalonia.Direct2D1 - - - - - + + + + + - + + + + + + + + + + - - \ No newline at end of file diff --git a/tests/Avalonia.Direct2D1.UnitTests/app.config b/tests/Avalonia.Direct2D1.UnitTests/app.config index 6af30494f2..e4df6c253e 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/app.config +++ b/tests/Avalonia.Direct2D1.UnitTests/app.config @@ -1,15 +1,15 @@ - + - - + + - - + + - \ No newline at end of file + diff --git a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj index 98a1312782..1f66290dd9 100644 --- a/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj +++ b/tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.0 + net47;netcoreapp2.0 Library diff --git a/tests/Avalonia.Input.UnitTests/KeyGestureParseTests.cs b/tests/Avalonia.Input.UnitTests/KeyGestureParseTests.cs deleted file mode 100644 index d26b03ad80..0000000000 --- a/tests/Avalonia.Input.UnitTests/KeyGestureParseTests.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; - -namespace Avalonia.Input.UnitTests -{ - public class KeyGestureParseTests - { - private static readonly Dictionary SampleData = new Dictionary - { - {"Ctrl+A", new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}}, - {" \tShift\t+Alt +B", new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Shift|InputModifiers.Alt} }, - {"Control++", new KeyGesture {Key = Key.OemPlus, Modifiers = InputModifiers.Control} } - }; - - - - [Fact] - public void Key_Gesture_Is_Able_To_Parse_Sample_Data() - { - foreach (var d in SampleData) - Assert.Equal(d.Value, KeyGesture.Parse(d.Key)); - } - } -} diff --git a/tests/Avalonia.Input.UnitTests/KeyGestureTests.cs b/tests/Avalonia.Input.UnitTests/KeyGestureTests.cs new file mode 100644 index 0000000000..006ed1140e --- /dev/null +++ b/tests/Avalonia.Input.UnitTests/KeyGestureTests.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Xunit; + +namespace Avalonia.Input.UnitTests +{ + public class KeyGestureTests + { + public static readonly IEnumerable SampleData = new object[][] + { + new object[]{"Ctrl+A", new KeyGesture {Key = Key.A, Modifiers = InputModifiers.Control}}, + new object[]{" \tShift\t+Alt +B", new KeyGesture {Key = Key.B, Modifiers = InputModifiers.Shift|InputModifiers.Alt} }, + new object[]{"Control++", new KeyGesture {Key = Key.OemPlus, Modifiers = InputModifiers.Control} } + }; + + + + [Theory] + [MemberData(nameof(SampleData))] + public void Key_Gesture_Is_Able_To_Parse_Sample_Data(string text, KeyGesture gesture) + { + Assert.Equal(gesture, KeyGesture.Parse(text)); + } + + [Theory] + [InlineData(Key.OemMinus, Key.Subtract)] + [InlineData(Key.OemPlus, Key.Add)] + [InlineData(Key.OemPeriod, Key.Decimal)] + public void Key_Gesture_Matches_NumPad_To_Regular_Digit(Key gestureKey, Key pressedKey) + { + var keyGesture = new KeyGesture + { + Key = gestureKey + }; + Assert.True(keyGesture.Matches(new KeyEventArgs + { + Key = pressedKey + })); + } + } +} diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs index 2b9b0baf01..b81b724e2a 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Arrows.cs @@ -6,8 +6,6 @@ using Xunit; namespace Avalonia.Input.UnitTests { - using Controls = Controls.Controls; - public class KeyboardNavigationTests_Arrows { [Fact] @@ -18,12 +16,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -33,7 +31,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -56,12 +54,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -71,7 +69,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { (next = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -94,12 +92,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -123,16 +121,16 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -144,7 +142,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { (next = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -167,7 +165,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), } @@ -187,17 +185,17 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -209,7 +207,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -232,12 +230,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -246,7 +244,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -269,12 +267,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -283,7 +281,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -306,12 +304,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -321,7 +319,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -343,12 +341,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -358,7 +356,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -380,12 +378,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.None, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -395,7 +393,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -418,12 +416,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, (next = new Button { Name = "Button2" }), @@ -433,7 +431,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -456,12 +454,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -471,7 +469,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { (current = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -495,12 +493,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -524,16 +522,16 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -545,7 +543,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { (current = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -568,16 +566,16 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { (current = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -589,7 +587,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Continue, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -632,12 +630,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), (current = new Button { Name = "Button2" }), @@ -647,7 +645,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -670,12 +668,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { (current = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -685,7 +683,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -708,12 +706,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), (current = new Button { Name = "Button2" }), @@ -723,7 +721,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -745,12 +743,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { (current = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -760,7 +758,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -783,7 +781,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { [KeyboardNavigation.DirectionalNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { (current = new Decorator { diff --git a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs index 2cc7a4281b..ad70dcd470 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardNavigationTests_Tab.cs @@ -6,8 +6,6 @@ using Xunit; namespace Avalonia.Input.UnitTests { - using Controls = Controls.Controls; - public class KeyboardNavigationTests_Tab { [Fact] @@ -18,11 +16,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -31,7 +29,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -54,11 +52,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -67,7 +65,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -90,11 +88,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -104,11 +102,11 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None, - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -133,11 +131,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -161,15 +159,15 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -180,7 +178,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -202,7 +200,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), } @@ -221,15 +219,15 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -240,7 +238,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -263,12 +261,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -277,7 +275,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -300,12 +298,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -314,7 +312,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -337,12 +335,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -351,7 +349,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -373,12 +371,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -387,7 +385,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -410,12 +408,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -424,7 +422,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -448,12 +446,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { (container = new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, - Children = new Controls + Children = { new Button { Name = "Button1" }, (next = new Button { Name = "Button2" }), @@ -462,7 +460,7 @@ namespace Avalonia.Input.UnitTests }), new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -487,12 +485,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None, - Children = new Controls + Children = { new Button { Name = "Button1" }, (current = new Button { Name = "Button2" }), @@ -501,7 +499,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -525,12 +523,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { (container = new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.None, - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -539,7 +537,7 @@ namespace Avalonia.Input.UnitTests }), new StackPanel { - Children = new Controls + Children = { (next = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -564,11 +562,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, (next = new Button { Name = "Button2" }), @@ -577,7 +575,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -600,11 +598,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -613,7 +611,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { (current = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -636,11 +634,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -664,15 +662,15 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -683,7 +681,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { (current = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -706,15 +704,15 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { (current = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -725,7 +723,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -767,12 +765,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), (current = new Button { Name = "Button2" }), @@ -781,7 +779,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -804,12 +802,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Cycle, - Children = new Controls + Children = { (current = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -818,7 +816,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -841,12 +839,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), (current = new Button { Name = "Button2" }), @@ -855,7 +853,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -877,12 +875,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { (current = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -891,7 +889,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button4" }, new Button { Name = "Button5" }, @@ -914,11 +912,11 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { - Children = new Controls + Children = { new Button { Name = "Button1" }, new Button { Name = "Button2" }, @@ -928,7 +926,7 @@ namespace Avalonia.Input.UnitTests new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, - Children = new Controls + Children = { new Button { Name = "Button4" }, (current = new Button { Name = "Button5" }), @@ -952,12 +950,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { (container = new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, - Children = new Controls + Children = { new Button { Name = "Button1" }, (next = new Button { Name = "Button2" }), @@ -966,7 +964,7 @@ namespace Avalonia.Input.UnitTests }), new StackPanel { - Children = new Controls + Children = { (current = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -991,12 +989,12 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { - Children = new Controls + Children = { new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Once, - Children = new Controls + Children = { (next = new Button { Name = "Button1" }), new Button { Name = "Button2" }, @@ -1005,7 +1003,7 @@ namespace Avalonia.Input.UnitTests }, new StackPanel { - Children = new Controls + Children = { (current = new Button { Name = "Button4" }), new Button { Name = "Button5" }, @@ -1028,7 +1026,7 @@ namespace Avalonia.Input.UnitTests var top = new StackPanel { [KeyboardNavigation.TabNavigationProperty] = KeyboardNavigationMode.Contained, - Children = new Controls + Children = { (current = new Decorator { diff --git a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj index 78d2128478..3fb2439af8 100644 --- a/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj +++ b/tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.0 + net47;netcoreapp2.0 Library diff --git a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj index 0020ff46d9..ae6f4d463a 100644 --- a/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj +++ b/tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.0 + net47;netcoreapp2.0 Library diff --git a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs index 361e7678be..4fce3fec0e 100644 --- a/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs +++ b/tests/Avalonia.Layout.UnitTests/LayoutManagerTests.cs @@ -275,10 +275,10 @@ namespace Avalonia.Layout.UnitTests { Child = panel = new StackPanel { - Children = new Controls.Controls - { - (border = new Border()) - } + Children = + { + (border = new Border()) + } } }; diff --git a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj index 46bd4ee324..7ecc3797ed 100644 --- a/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj +++ b/tests/Avalonia.LeakTests/Avalonia.LeakTests.csproj @@ -9,7 +9,7 @@ Properties Avalonia.LeakTests Avalonia.LeakTests - v4.6.1 + v4.7 512 diff --git a/tests/Avalonia.LeakTests/app.config b/tests/Avalonia.LeakTests/app.config index 01de951354..71e6d0a02e 100644 --- a/tests/Avalonia.LeakTests/app.config +++ b/tests/Avalonia.LeakTests/app.config @@ -1,15 +1,15 @@ - + - - + + - - + + - \ No newline at end of file + diff --git a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj index 5d3b04e24b..4db9bc45ad 100644 --- a/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj +++ b/tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.0 + net47;netcoreapp2.0 Library diff --git a/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs b/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs index a5414f1e8c..b3e983036d 100644 --- a/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs +++ b/tests/Avalonia.Markup.UnitTests/ControlLocatorTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Markup.UnitTests { Child = new StackPanel { - Children = new Controls.Controls + Children = { (target = new TextBlock { Name = "target" }), (relativeTo = new TextBlock { Name = "start" }), @@ -49,7 +49,7 @@ namespace Avalonia.Markup.UnitTests { Child = (panel = new StackPanel { - Children = new Controls.Controls + Children = { (relativeTo = new TextBlock { @@ -84,7 +84,7 @@ namespace Avalonia.Markup.UnitTests { Child = panel = new StackPanel { - Children = new Controls.Controls + Children = { (target = new TextBlock { Name = "target" }), (relativeTo = new TextBlock { Name = "start" }), @@ -114,7 +114,7 @@ namespace Avalonia.Markup.UnitTests { Child = new StackPanel { - Children = new Controls.Controls + Children = { (relativeTo = new TextBlock { @@ -129,7 +129,7 @@ namespace Avalonia.Markup.UnitTests { Child = new StackPanel { - Children = new Controls.Controls + Children = { (target2 = new TextBlock { Name = "target" }), } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index eb6dc8b5e5..02fbf23687 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -1,6 +1,6 @@  - net461;netcoreapp2.0 + net47;netcoreapp2.0 Library @@ -21,6 +21,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 230e61f300..71c5385c23 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/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,167 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data Assert.Equal("foo", target.Content); } + [Fact] + public void StyledProperty_SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + var target = new StyledPropertyClass(); + + target.Bind(StyledPropertyClass.DoubleValueProperty, + new Binding("Value") { Mode = BindingMode.TwoWay, Source = viewModel }); + + var child = new StyledPropertyClass(); + + child.Bind(StyledPropertyClass.DoubleValueProperty, + new Binding("DoubleValue") + { + Mode = BindingMode.TwoWay, + Source = target + }); + + Assert.Equal(1, viewModel.SetterInvokedCount); + + //here in real life stack overflow exception is thrown issue #855 and #824 + target.DoubleValue = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.DoubleValue); + Assert.Equal(expected, child.DoubleValue); + } + + [Fact] + public void SetValue_Should_Not_Cause_StackOverflow_And_Have_Correct_Values() + { + var viewModel = new TestStackOverflowViewModel() + { + Value = 50 + }; + + var target = new DirectPropertyClass(); + + target.Bind(DirectPropertyClass.DoubleValueProperty, new Binding("Value") + { + Mode = BindingMode.TwoWay, + Source = viewModel + }); + + var child = new DirectPropertyClass(); + + child.Bind(DirectPropertyClass.DoubleValueProperty, + new Binding("DoubleValue") + { + Mode = BindingMode.TwoWay, + Source = target + }); + + Assert.Equal(1, viewModel.SetterInvokedCount); + + //here in real life stack overflow exception is thrown issue #855 and #824 + target.DoubleValue = 51.001; + + Assert.Equal(2, viewModel.SetterInvokedCount); + + double expected = 51; + + Assert.Equal(expected, viewModel.Value); + Assert.Equal(expected, target.DoubleValue); + Assert.Equal(expected, child.DoubleValue); + } + + private class StyledPropertyClass : AvaloniaObject + { + public static readonly StyledProperty DoubleValueProperty = + AvaloniaProperty.Register(nameof(DoubleValue)); + + public double DoubleValue + { + get { return GetValue(DoubleValueProperty); } + set { SetValue(DoubleValueProperty, value); } + } + } + + private class DirectPropertyClass : AvaloniaObject + { + public static readonly DirectProperty DoubleValueProperty = + AvaloniaProperty.RegisterDirect( + nameof(DoubleValue), + o => o.DoubleValue, + (o, v) => o.DoubleValue = v); + + private double _doubleValue; + public double DoubleValue + { + get { return _doubleValue; } + set { SetAndRaise(DoubleValueProperty, ref _doubleValue, value); } + } + } + + private class TestStackOverflowViewModel : INotifyPropertyChanged + { + public int SetterInvokedCount { get; private set; } + + public const int MaxInvokedCount = 1000; + + private double _value; + + public event PropertyChangedEventHandler PropertyChanged; + + public double Value + { + get { return _value; } + set + { + if (_value != value) + { + SetterInvokedCount++; + if (SetterInvokedCount < MaxInvokedCount) + { + _value = (int)value; + if (_value > 75) _value = 75; + if (_value < 25) _value = 25; + } + else + { + _value = value; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); + } + } + } + } + + + [Fact] + public void Binding_With_Null_Path_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.DataContext = "foo"; + window.ApplyTemplate(); + + Assert.Equal("foo", textBlock.Text); + } + } + private class TwoWayBindingTest : Control { public static readonly StyledProperty TwoWayProperty = diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs index 40a4a6f0b3..d582964987 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_ElementName.cs @@ -18,7 +18,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data { Child = new StackPanel { - Children = new Controls.Controls + Children = { new TextBlock { @@ -54,7 +54,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data { Child = new StackPanel { - Children = new Controls.Controls + Children = { (source = new TextBlock { @@ -89,7 +89,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data { Child = stackPanel = new StackPanel { - Children = new Controls.Controls + Children = { (target = new TextBlock { @@ -126,7 +126,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Data { Child = stackPanel = new StackPanel { - Children = new Controls.Controls + Children = { (target = new ContentControl { diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs index 197afe46ee..ccb13039f1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_TemplatedParent.cs +++ b/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 = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var button = window.FindControl