diff --git a/.gitignore b/.gitignore index 971c945246..b5a46e16f4 100644 --- a/.gitignore +++ b/.gitignore @@ -198,3 +198,11 @@ info.plist build-intermediate obj-Direct2D1/ obj-Skia/ + +################## +# Vim +################## +.vim +coc-settings.json +.ccls-cache +.ccls diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3f4fbb0d50..7e3532ee23 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -102,7 +102,7 @@ jobs: - job: Windows pool: - vmImage: 'vs2017-win2016' + vmImage: 'windows-2019' steps: - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/build-native.sh b/build-native.sh old mode 100644 new mode 100755 diff --git a/build/Base.props b/build/Base.props index e565ab1664..a60373ebb3 100644 --- a/build/Base.props +++ b/build/Base.props @@ -2,4 +2,4 @@ - \ No newline at end of file + diff --git a/build/SharedVersion.props b/build/SharedVersion.props index 4f0b1f0a5b..76abcf6912 100644 --- a/build/SharedVersion.props +++ b/build/SharedVersion.props @@ -2,7 +2,7 @@ xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> Avalonia - 0.8.1 + 0.8.999 Copyright 2019 © The AvaloniaUI Project https://github.com/AvaloniaUI/Avalonia/blob/master/licence.md https://github.com/AvaloniaUI/Avalonia/ diff --git a/native/Avalonia.Native/src/OSX/KeyTransform.mm b/native/Avalonia.Native/src/OSX/KeyTransform.mm index 7486aaad69..971bcf24f8 100644 --- a/native/Avalonia.Native/src/OSX/KeyTransform.mm +++ b/native/Avalonia.Native/src/OSX/KeyTransform.mm @@ -26,7 +26,7 @@ const int kVK_ANSI_3 = 0x14; const int kVK_ANSI_4 = 0x15; const int kVK_ANSI_6 = 0x16; const int kVK_ANSI_5 = 0x17; -//const int kVK_ANSI_Equal = 0x18; +const int kVK_ANSI_Equal = 0x18; const int kVK_ANSI_9 = 0x19; const int kVK_ANSI_7 = 0x1A; const int kVK_ANSI_Minus = 0x1B; @@ -45,11 +45,11 @@ const int kVK_ANSI_K = 0x28; const int kVK_ANSI_Semicolon = 0x29; const int kVK_ANSI_Backslash = 0x2A; const int kVK_ANSI_Comma = 0x2B; -//const int kVK_ANSI_Slash = 0x2C; +const int kVK_ANSI_Slash = 0x2C; const int kVK_ANSI_N = 0x2D; const int kVK_ANSI_M = 0x2E; const int kVK_ANSI_Period = 0x2F; -//const int kVK_ANSI_Grave = 0x32; +const int kVK_ANSI_Grave = 0x32; const int kVK_ANSI_KeypadDecimal = 0x41; const int kVK_ANSI_KeypadMultiply = 0x43; const int kVK_ANSI_KeypadPlus = 0x45; @@ -57,7 +57,7 @@ const int kVK_ANSI_KeypadClear = 0x47; const int kVK_ANSI_KeypadDivide = 0x4B; const int kVK_ANSI_KeypadEnter = 0x4C; const int kVK_ANSI_KeypadMinus = 0x4E; -//const int kVK_ANSI_KeypadEquals = 0x51; +const int kVK_ANSI_KeypadEquals = 0x51; const int kVK_ANSI_Keypad0 = 0x52; const int kVK_ANSI_Keypad1 = 0x53; const int kVK_ANSI_Keypad2 = 0x54; @@ -121,7 +121,7 @@ const int kVK_UpArrow = 0x7E; //const int kVK_JIS_Underscore = 0x5E; //const int kVK_JIS_KeypadComma = 0x5F; //const int kVK_JIS_Eisu = 0x66; -//const int kVK_JIS_Kana = 0x68; +const int kVK_JIS_Kana = 0x68; std::map s_KeyMap = { @@ -148,7 +148,7 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_4, D4}, {kVK_ANSI_6, D6}, {kVK_ANSI_5, D5}, - //{kVK_ANSI_Equal, ?}, + {kVK_ANSI_Equal, OemPlus}, {kVK_ANSI_9, D9}, {kVK_ANSI_7, D7}, {kVK_ANSI_Minus, OemMinus}, @@ -167,11 +167,11 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_Semicolon, OemSemicolon}, {kVK_ANSI_Backslash, OemBackslash}, {kVK_ANSI_Comma, OemComma}, - //{kVK_ANSI_Slash, ?}, + {kVK_ANSI_Slash, Oem2}, {kVK_ANSI_N, N}, {kVK_ANSI_M, M}, {kVK_ANSI_Period, OemPeriod}, - //{kVK_ANSI_Grave, ?}, + {kVK_ANSI_Grave, OemTilde}, {kVK_ANSI_KeypadDecimal, Decimal}, {kVK_ANSI_KeypadMultiply, Multiply}, {kVK_ANSI_KeypadPlus, OemPlus}, @@ -179,7 +179,7 @@ const int kVK_UpArrow = 0x7E; {kVK_ANSI_KeypadDivide, Divide}, {kVK_ANSI_KeypadEnter, AvnKeyEnter}, {kVK_ANSI_KeypadMinus, OemMinus}, - //{kVK_ANSI_KeypadEquals, ?}, + {kVK_ANSI_KeypadEquals, OemPlus}, {kVK_ANSI_Keypad0, NumPad0}, {kVK_ANSI_Keypad1, NumPad1}, {kVK_ANSI_Keypad2, NumPad2}, @@ -237,5 +237,6 @@ const int kVK_UpArrow = 0x7E; {kVK_LeftArrow, Left}, {kVK_RightArrow, Right}, {kVK_DownArrow, Down}, - {kVK_UpArrow, Up} + {kVK_UpArrow, Up}, + {kVK_JIS_Kana, AvnKeyKanaMode}, }; diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index a99ac4d026..dd2f27116d 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -89,6 +89,29 @@ partial class Build : NukeBuild } + IReadOnlyCollection MsBuildCommon( + string projectFile, + Configure configurator = null) + { + return MSBuild(projectFile, c => + { + // This is required for VS2019 image on Azure Pipelines + if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure) + { + var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64"); + if (javaSdk != null) + c = c.AddProperty("JavaSdkDirectory", javaSdk); + } + + c = c.AddProperty("PackageVersion", Parameters.Version) + .AddProperty("iOSRoslynPathHackRequired", "true") + .SetToolPath(MsBuildExe.Value) + .SetConfiguration(Parameters.Configuration) + .SetVerbosity(MSBuildVerbosity.Minimal); + c = configurator?.Invoke(c) ?? c; + return c; + }); + } Target Clean => _ => _.Executes(() => { DeleteDirectories(Parameters.BuildDirs); @@ -105,13 +128,8 @@ partial class Build : NukeBuild .Executes(() => { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c + MsBuildCommon(Parameters.MSBuildSolution, c => c .SetArgumentConfigurator(a => a.Add("/r")) - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) .AddTargets("Build") ); @@ -237,12 +255,7 @@ partial class Build : NukeBuild { if (Parameters.IsRunningOnWindows) - MSBuild(Parameters.MSBuildSolution, c => c - .SetConfiguration(Parameters.Configuration) - .SetVerbosity(MSBuildVerbosity.Minimal) - .AddProperty("PackageVersion", Parameters.Version) - .AddProperty("iOSRoslynPathHackRequired", "true") - .SetToolPath(MsBuildExe.Value) + MsBuildCommon(Parameters.MSBuildSolution, c => c .AddTargets("Pack")); else DotNetPack(Parameters.MSBuildSolution, c => diff --git a/readme.md b/readme.md index 7a03fda384..ee44a0cc3f 100644 --- a/readme.md +++ b/readme.md @@ -32,9 +32,6 @@ Install-Package Avalonia.Desktop ## Bleeding Edge Builds -Try out the latest build of Avalonia available for download here: -https://ci.appveyor.com/project/AvaloniaUI/Avalonia/branch/master/artifacts - or use nightly build feeds as described here: https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed diff --git a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj index d8f0f39977..bc76a39f08 100644 --- a/samples/ControlCatalog.Android/ControlCatalog.Android.csproj +++ b/samples/ControlCatalog.Android/ControlCatalog.Android.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml index 1cddb9d295..8699508320 100644 --- a/samples/ControlCatalog/MainView.xaml +++ b/samples/ControlCatalog/MainView.xaml @@ -36,6 +36,7 @@ + diff --git a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml index 0ca3567970..f90a0c4658 100644 --- a/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml +++ b/samples/ControlCatalog/Pages/AutoCompleteBoxPage.xaml @@ -37,10 +37,6 @@ - - + + TabStrip + A control which displays a selectable strip of tabs + + + + Defined in XAML + + Item 1 + Item 2 + Disabled + + + + + Dynamically generated + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/TabStripPage.xaml.cs b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs new file mode 100644 index 0000000000..f0630cf534 --- /dev/null +++ b/samples/ControlCatalog/Pages/TabStripPage.xaml.cs @@ -0,0 +1,45 @@ +using System; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.Media.Imaging; +using Avalonia.Platform; + +namespace ControlCatalog.Pages +{ + public class TabStripPage : UserControl + { + public TabStripPage() + { + InitializeComponent(); + + DataContext = new[] + { + new TabStripItemViewModel + { + Header = "Item 1", + }, + new TabStripItemViewModel + { + Header = "Item 2", + }, + new TabStripItemViewModel + { + Header = "Disabled", + IsEnabled = false, + }, + }; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private class TabStripItemViewModel + { + public string Header { get; set; } + public bool IsEnabled { get; set; } = true; + } + } +} diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 3047b1e519..3513e94107 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -29,8 +29,7 @@ Name="PART_ItemsPresenter" Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" - ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> + ItemTemplate="{TemplateBinding ItemTemplate}"> - \ No newline at end of file + diff --git a/src/Android/Avalonia.Android/Avalonia.Android.csproj b/src/Android/Avalonia.Android/Avalonia.Android.csproj index 9c3d4fb3a1..0089ea3b8d 100644 --- a/src/Android/Avalonia.Android/Avalonia.Android.csproj +++ b/src/Android/Avalonia.Android/Avalonia.Android.csproj @@ -1,6 +1,6 @@  - monoandroid44 + monoandroid80 true diff --git a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj index c2c0cd4301..1b2b205d45 100644 --- a/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj +++ b/src/Android/Avalonia.AndroidTestApplication/Avalonia.AndroidTestApplication.csproj @@ -16,7 +16,7 @@ Resources\Resource.Designer.cs Off False - v4.4 + v8.0 Properties\AndroidManifest.xml @@ -150,6 +150,7 @@ + diff --git a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs index 990a4b04f2..0ffd6a9539 100644 --- a/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs +++ b/src/Avalonia.Base/Data/Converters/DefaultValueConverter.cs @@ -31,7 +31,7 @@ namespace Avalonia.Data.Converters { if (value == null) { - return AvaloniaProperty.UnsetValue; + return targetType.IsValueType ? AvaloniaProperty.UnsetValue : null; } if (typeof(ICommand).IsAssignableFrom(targetType) && value is Delegate d && d.Method.GetParameters().Length <= 1) diff --git a/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs new file mode 100644 index 0000000000..f321b2b2f1 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SynchronousCompletionAsyncResult.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Avalonia.Utilities +{ + /// + /// A task-like operation that is guaranteed to finish continuations synchronously, + /// can be used for parametrized one-shot events + /// + public struct SynchronousCompletionAsyncResult : INotifyCompletion + { + private readonly SynchronousCompletionAsyncResultSource _source; + private readonly T _result; + private readonly bool _isValid; + internal SynchronousCompletionAsyncResult(SynchronousCompletionAsyncResultSource source) + { + _source = source; + _result = default; + _isValid = true; + } + + public SynchronousCompletionAsyncResult(T result) + { + _result = result; + _source = null; + _isValid = true; + } + + static void ThrowNotInitialized() => + throw new InvalidOperationException("This SynchronousCompletionAsyncResult was not initialized"); + + public bool IsCompleted + { + get + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null || _source.IsCompleted; + } + } + + public T GetResult() + { + if (!_isValid) + ThrowNotInitialized(); + return _source == null ? _result : _source.Result; + } + + + public void OnCompleted(Action continuation) + { + if (!_isValid) + ThrowNotInitialized(); + if (_source == null) + continuation(); + else + _source.OnCompleted(continuation); + } + + public SynchronousCompletionAsyncResult GetAwaiter() => this; + } + + /// + /// Source for incomplete SynchronousCompletionAsyncResult + /// + /// + public class SynchronousCompletionAsyncResultSource + { + private T _result; + internal bool IsCompleted { get; private set; } + public SynchronousCompletionAsyncResult AsyncResult => new SynchronousCompletionAsyncResult(this); + + internal T Result => IsCompleted ? + _result : + throw new InvalidOperationException("Asynchronous operation is not yet completed"); + + private List _continuations; + + internal void OnCompleted(Action continuation) + { + if(_continuations==null) + _continuations = new List(); + _continuations.Add(continuation); + } + + public void SetResult(T result) + { + if (IsCompleted) + throw new InvalidOperationException("Asynchronous operation is already completed"); + _result = result; + IsCompleted = true; + if(_continuations!=null) + foreach (var c in _continuations) + c(); + _continuations = null; + } + + public void TrySetResult(T result) + { + if(IsCompleted) + return; + SetResult(result); + } + } +} diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index c54d8e19a1..0dd52c9b48 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -75,9 +75,9 @@ namespace Avalonia.Build.Tasks .First(c => c.Parameters.Count == 1)); var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); - var rootServiceProviderField = asm.MainModule.ImportReference( - typeSystem.GetTypeReference(runtimeHelpers).Resolve().Fields - .First(x => x.Name == "RootServiceProviderV1")); + var createRootServiceProviderMethod = asm.MainModule.ImportReference( + typeSystem.GetTypeReference(runtimeHelpers).Resolve().Methods + .First(x => x.Name == "CreateRootServiceProviderV2")); var loaderDispatcherDef = new TypeDefinition("CompiledAvaloniaXaml", "!XamlLoader", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); @@ -211,7 +211,7 @@ namespace Avalonia.Build.Tasks trampoline.Parameters.Add(new ParameterDefinition(classTypeDefinition)); classTypeDefinition.Methods.Add(trampoline); - var regularStart = Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField); + var regularStart = Instruction.Create(OpCodes.Call, createRootServiceProviderMethod); trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Ldsfld, designLoaderField)); trampoline.Body.Instructions.Add(Instruction.Create(OpCodes.Brfalse, regularStart)); @@ -307,7 +307,7 @@ namespace Avalonia.Build.Tasks i.Add(Instruction.Create(OpCodes.Newobj, parameterlessConstructor)); else { - i.Add(Instruction.Create(OpCodes.Ldsfld, rootServiceProviderField)); + i.Add(Instruction.Create(OpCodes.Call, createRootServiceProviderMethod)); i.Add(Instruction.Create(OpCodes.Call, compiledBuildMethod)); } diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index b87e10d284..1e2fc9f9d0 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -345,7 +345,6 @@ namespace Avalonia.Controls /// private IDisposable _collectionChangeSubscription; - private IMemberSelector _valueMemberSelector; private Func>> _asyncPopulator; private CancellationTokenSource _populationCancellationTokenSource; @@ -541,12 +540,6 @@ namespace Avalonia.Controls o => o.Items, (o, v) => o.Items = v); - public static readonly DirectProperty ValueMemberSelectorProperty = - AvaloniaProperty.RegisterDirect( - nameof(ValueMemberSelector), - o => o.ValueMemberSelector, - (o, v) => o.ValueMemberSelector = v); - public static readonly DirectProperty>>> AsyncPopulatorProperty = AvaloniaProperty.RegisterDirect>>>( nameof(AsyncPopulator), @@ -795,7 +788,7 @@ namespace Avalonia.Controls var template = new FuncDataTemplate( typeof(object), - o => + (o, _) => { var control = new ContentControl(); control.Bind(ContentControl.ContentProperty, value); @@ -958,20 +951,6 @@ namespace Avalonia.Controls } } - /// - /// Gets or sets the MemberSelector that is used to get values for - /// display in the text portion of the - /// control. - /// - /// The MemberSelector that is used to get values for display in - /// the text portion of the - /// control. - public IMemberSelector ValueMemberSelector - { - get { return _valueMemberSelector; } - set { SetAndRaise(ValueMemberSelectorProperty, ref _valueMemberSelector, value); } - } - /// /// Gets or sets the selected item in the drop-down. /// @@ -1841,11 +1820,6 @@ namespace Avalonia.Controls return _valueBindingEvaluator.GetDynamicValue(value) ?? String.Empty; } - if (_valueMemberSelector != null) - { - value = _valueMemberSelector.Select(value); - } - return value == null ? String.Empty : value.ToString(); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 5d427df5a6..f32b8fabc6 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -333,8 +333,7 @@ namespace Avalonia.Controls } else { - var selector = MemberSelector; - SelectionBoxItem = selector != null ? selector.Select(item) : item; + SelectionBoxItem = item; } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92293a32d6..58b4324a3e 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -1,12 +1,12 @@ using System; using System.ComponentModel; using System.Linq; -using System.Reactive.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -90,9 +90,14 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (IsOpen) + { + return; + } + if (_popup == null) { - _popup = new Popup() + _popup = new Popup { PlacementMode = PlacementMode.Pointer, PlacementTarget = control, @@ -107,7 +112,14 @@ namespace Avalonia.Controls ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// @@ -115,13 +127,15 @@ namespace Avalonia.Controls /// public override void Close() { + if (!IsOpen) + { + return; + } + if (_popup != null && _popup.IsVisible) { _popup.IsOpen = false; } - - SelectedIndex = -1; - IsOpen = false; } protected override IItemContainerGenerator CreateItemContainerGenerator() @@ -129,6 +143,18 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + private void CloseCore() + { + SelectedIndex = -1; + IsOpen = false; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } + private void PopupOpened(object sender, EventArgs e) { Focus(); @@ -145,8 +171,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu.IsOpen = false; - contextMenu.SelectedIndex = -1; + contextMenu.CloseCore(); } } diff --git a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs index 83d1c4aae3..2d48a7d33b 100644 --- a/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs +++ b/src/Avalonia.Controls/Embedding/EmbeddableControlRoot.cs @@ -8,7 +8,7 @@ using JetBrains.Annotations; namespace Avalonia.Controls.Embedding { - public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, INameScope, IDisposable + public class EmbeddableControlRoot : TopLevel, IStyleable, IFocusScope, IDisposable { public EmbeddableControlRoot(IEmbeddableWindowImpl impl) : base(impl) { @@ -51,25 +51,6 @@ namespace Avalonia.Controls.Embedding return rv; } - private readonly NameScope _nameScope = new NameScope(); - public event EventHandler Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - public event EventHandler Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - public void Register(string name, object element) => _nameScope.Register(name, element); - - public object Find(string name) => _nameScope.Find(name); - - public void Unregister(string name) => _nameScope.Unregister(name); - Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); public void Dispose() => PlatformImpl?.Dispose(); } diff --git a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs index c4f83ffd54..d326ab5734 100644 --- a/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs +++ b/src/Avalonia.Controls/Embedding/Offscreen/OffscreenTopLevel.cs @@ -30,25 +30,6 @@ namespace Avalonia.Controls.Embedding.Offscreen init.EndInit(); } } - - private readonly NameScope _nameScope = new NameScope(); - public event EventHandler Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - public event EventHandler Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - public void Register(string name, object element) => _nameScope.Register(name, element); - - public object Find(string name) => _nameScope.Find(name); - - public void Unregister(string name) => _nameScope.Unregister(name); Type IStyleable.StyleKey => typeof(EmbeddableControlRoot); public void Dispose() diff --git a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs index 653a4f5dcb..2d6757219d 100644 --- a/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/IItemContainerGenerator.cs @@ -49,12 +49,8 @@ namespace Avalonia.Controls.Generators /// The index of the item of data in the control's items. /// /// The item. - /// An optional member selector. /// The created controls. - ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector); + ItemContainerInfo Materialize(int index, object item); /// /// Removes a set of created containers. @@ -84,11 +80,7 @@ namespace Avalonia.Controls.Generators /// The removed containers. IEnumerable RemoveRange(int startingIndex, int count); - bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector); + bool TryRecycle(int oldIndex, int newIndex, object item); /// /// Clears all created containers and returns the removed controls. diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs index f1a1f94a01..4fd6f4135c 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator.cs @@ -54,13 +54,9 @@ namespace Avalonia.Controls.Generators public virtual Type ContainerType => null; /// - public ItemContainerInfo Materialize( - int index, - object item, - IMemberSelector selector) + public ItemContainerInfo Materialize(int index, object item) { - var i = selector != null ? selector.Select(item) : item; - var container = new ItemContainerInfo(CreateContainer(i), item, index); + var container = new ItemContainerInfo(CreateContainer(item), item, index); _containers.Add(container.Index, container); Materialized?.Invoke(this, new ItemContainerEventArgs(container)); @@ -138,14 +134,7 @@ namespace Avalonia.Controls.Generators } /// - public virtual bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) - { - return false; - } + public virtual bool TryRecycle(int oldIndex, int newIndex, object item) => false; /// public virtual IEnumerable Clear() diff --git a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs index 320d6c8faf..d1d1c2b172 100644 --- a/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs +++ b/src/Avalonia.Controls/Generators/ItemContainerGenerator`1.cs @@ -79,11 +79,7 @@ namespace Avalonia.Controls.Generators } /// - public override bool TryRecycle( - int oldIndex, - int newIndex, - object item, - IMemberSelector selector) + public override bool TryRecycle(int oldIndex, int newIndex, object item) { var container = ContainerFromIndex(oldIndex); @@ -92,16 +88,14 @@ namespace Avalonia.Controls.Generators throw new IndexOutOfRangeException("Could not recycle container: not materialized."); } - var i = selector != null ? selector.Select(item) : item; - - container.SetValue(ContentProperty, i); + container.SetValue(ContentProperty, item); if (!(item is IControl)) { - container.DataContext = i; + container.DataContext = item; } - var info = MoveContainer(oldIndex, newIndex, i); + var info = MoveContainer(oldIndex, newIndex, item); RaiseRecycled(new ItemContainerEventArgs(info)); return true; diff --git a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs index 304c86dbf7..c06a64443c 100644 --- a/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs +++ b/src/Avalonia.Controls/Generators/TreeItemContainerGenerator.cs @@ -92,7 +92,6 @@ namespace Avalonia.Controls.Generators result.DataContext = item; } - NameScope.SetNameScope((Control)(object)result, new NameScope()); Index.Add(item, result); return result; @@ -118,16 +117,22 @@ namespace Avalonia.Controls.Generators return base.RemoveRange(startingIndex, count); } - public override bool TryRecycle(int oldIndex, int newIndex, object item, IMemberSelector selector) + public override bool TryRecycle(int oldIndex, int newIndex, object item) => false; + + class WrapperTreeDataTemplate : ITreeDataTemplate { - return false; + private readonly IDataTemplate _inner; + public WrapperTreeDataTemplate(IDataTemplate inner) => _inner = inner; + public IControl Build(object param) => _inner.Build(param); + public bool SupportsRecycling => _inner.SupportsRecycling; + public bool Match(object data) => _inner.Match(data); + public InstancedBinding ItemsSelector(object item) => null; } private ITreeDataTemplate GetTreeDataTemplate(object item, IDataTemplate primary) { var template = Owner.FindDataTemplate(item, primary) ?? FuncDataTemplate.Default; - var treeTemplate = template as ITreeDataTemplate ?? - new FuncTreeDataTemplate(typeof(object), template.Build, x => null); + var treeTemplate = template as ITreeDataTemplate ?? new WrapperTreeDataTemplate(template); return treeTemplate; } } diff --git a/src/Avalonia.Controls/ItemsControl.cs b/src/Avalonia.Controls/ItemsControl.cs index a292ff7d0a..902e55bde6 100644 --- a/src/Avalonia.Controls/ItemsControl.cs +++ b/src/Avalonia.Controls/ItemsControl.cs @@ -54,12 +54,6 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemTemplateProperty = AvaloniaProperty.Register(nameof(ItemTemplate)); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - AvaloniaProperty.Register(nameof(MemberSelector)); - private IEnumerable _items = new AvaloniaList(); private int _itemCount; private IItemContainerGenerator _itemContainerGenerator; @@ -144,15 +138,6 @@ namespace Avalonia.Controls set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the items presenter control. /// diff --git a/src/Avalonia.Controls/ListBox.cs b/src/Avalonia.Controls/ListBox.cs index 3150b6be91..f26cd47bcb 100644 --- a/src/Avalonia.Controls/ListBox.cs +++ b/src/Avalonia.Controls/ListBox.cs @@ -68,7 +68,13 @@ namespace Avalonia.Controls /// public new IList SelectedItems => base.SelectedItems; - /// + /// + /// Gets or sets the selection mode. + /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// public new SelectionMode SelectionMode { get { return base.SelectionMode; } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..b60a97e1c8 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -40,37 +40,41 @@ namespace Avalonia.Controls /// public override void Close() { - if (IsOpen) + if (!IsOpen) { - foreach (var i in ((IMenu)this).SubItems) - { - i.Close(); - } - - IsOpen = false; - SelectedIndex = -1; + return; + } - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuClosedEvent, - Source = this, - }); + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); } + + IsOpen = false; + SelectedIndex = -1; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); } /// public override void Open() { - if (!IsOpen) + if (IsOpen) { - IsOpen = true; - - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuOpenedEvent, - Source = this, - }); + return; } + + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index d6eb40360b..8eed58bb4d 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -7,7 +7,6 @@ using System.Linq; using Avalonia.Controls.Generators; using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; -using Avalonia.Controls.Templates; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; @@ -31,13 +30,13 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); private bool _isOpen; diff --git a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs index a3123cf8c6..dedab3e43e 100644 --- a/src/Avalonia.Controls/Presenters/CarouselPresenter.cs +++ b/src/Avalonia.Controls/Presenters/CarouselPresenter.cs @@ -213,7 +213,7 @@ namespace Avalonia.Controls.Presenters if (container == null && IsVirtualized) { var item = Items.Cast().ElementAt(index); - var materialized = ItemContainerGenerator.Materialize(index, item, MemberSelector); + var materialized = ItemContainerGenerator.Materialize(index, item); Panel.Children.Add(materialized.ContainerControl); container = materialized.ContainerControl; } diff --git a/src/Avalonia.Controls/Presenters/ContentPresenter.cs b/src/Avalonia.Controls/Presenters/ContentPresenter.cs index 49f268c128..c2690d503d 100644 --- a/src/Avalonia.Controls/Presenters/ContentPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ContentPresenter.cs @@ -325,12 +325,6 @@ namespace Avalonia.Controls.Presenters { _dataTemplate = dataTemplate; newChild = _dataTemplate.Build(content); - - // Give the new control its own name scope. - if (newChild is Control controlResult) - { - NameScope.SetNameScope(controlResult, new NameScope()); - } } } else diff --git a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs index 035d404dec..6e72908e6b 100644 --- a/src/Avalonia.Controls/Presenters/ItemContainerSync.cs +++ b/src/Avalonia.Controls/Presenters/ItemContainerSync.cs @@ -88,7 +88,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs index 413855bcc6..56f64779f6 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerNone.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls.Presenters foreach (var item in items) { - var i = generator.Materialize(index++, item, Owner.MemberSelector); + var i = generator.Materialize(index++, item); if (i.ContainerControl != null) { diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs index d11ce9a7ea..b8b8094582 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizerSimple.cs @@ -314,7 +314,6 @@ namespace Avalonia.Controls.Presenters if (!panel.IsFull && Items != null && panel.IsAttachedToVisualTree) { - var memberSelector = Owner.MemberSelector; var index = NextIndex; var step = 1; @@ -337,7 +336,7 @@ namespace Avalonia.Controls.Presenters } } - var materialized = generator.Materialize(index, Items.ElementAt(index), memberSelector); + var materialized = generator.Materialize(index, Items.ElementAt(index)); if (step == 1) { @@ -383,7 +382,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; var containers = generator.Containers.ToList(); var itemIndex = FirstIndex; @@ -393,7 +391,7 @@ namespace Avalonia.Controls.Presenters if (!object.Equals(container.Item, item)) { - if (!generator.TryRecycle(itemIndex, itemIndex, item, selector)) + if (!generator.TryRecycle(itemIndex, itemIndex, item)) { throw new NotImplementedException(); } @@ -420,7 +418,6 @@ namespace Avalonia.Controls.Presenters { var panel = VirtualizingPanel; var generator = Owner.ItemContainerGenerator; - var selector = Owner.MemberSelector; //validate delta it should never overflow last index or generate index < 0 delta = MathUtilities.Clamp(delta, -FirstIndex, ItemCount - FirstIndex - panel.Children.Count); @@ -437,7 +434,7 @@ namespace Avalonia.Controls.Presenters var item = Items.ElementAt(newItemIndex); - if (!generator.TryRecycle(oldItemIndex, newItemIndex, item, selector)) + if (!generator.TryRecycle(oldItemIndex, newItemIndex, item)) { throw new NotImplementedException(); } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index b4b792139d..ea56a0c6fc 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -35,12 +35,6 @@ namespace Avalonia.Controls.Presenters public static readonly StyledProperty ItemTemplateProperty = ItemsControl.ItemTemplateProperty.AddOwner(); - /// - /// Defines the property. - /// - public static readonly StyledProperty MemberSelectorProperty = - ItemsControl.MemberSelectorProperty.AddOwner(); - private IEnumerable _items; private IDisposable _itemsSubscription; private bool _createdPanel; @@ -127,15 +121,6 @@ namespace Avalonia.Controls.Presenters set { SetValue(ItemTemplateProperty, value); } } - /// - /// Selects a member from to use as the list item. - /// - public IMemberSelector MemberSelector - { - get { return GetValue(MemberSelectorProperty); } - set { SetValue(MemberSelectorProperty, value); } - } - /// /// Gets the panel used to display the items. /// diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index e02d46c1df..058658357f 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -6,7 +6,6 @@ using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; using Avalonia.Interactivity; -using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Metadata; using Avalonia.VisualTree; @@ -270,9 +269,10 @@ namespace Avalonia.Controls.Primitives _popupRoot.SnapInsideScreenEdges(); } - _ignoreIsOpenChanged = true; - IsOpen = true; - _ignoreIsOpenChanged = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = true; + } Opened?.Invoke(this, EventArgs.Empty); } @@ -305,7 +305,11 @@ namespace Avalonia.Controls.Primitives _popupRoot.Hide(); } - IsOpen = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + Closed?.Invoke(this, EventArgs.Empty); } @@ -467,5 +471,26 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private IgnoreIsOpenScope BeginIgnoringIsOpen() + { + return new IgnoreIsOpenScope(this); + } + + private readonly struct IgnoreIsOpenScope : IDisposable + { + private readonly Popup _owner; + + public IgnoreIsOpenScope(Popup owner) + { + _owner = owner; + _owner._ignoreIsOpenChanged = true; + } + + public void Dispose() + { + _owner._ignoreIsOpenChanged = false; + } + } } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 2c0f67fada..188685f796 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -222,6 +222,10 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the selection mode. /// + /// + /// Note that the selection mode only applies to selections made via user interaction. + /// Multiple selections can be made programatically regardless of the value of this property. + /// protected SelectionMode SelectionMode { get { return GetValue(SelectionModeProperty); } @@ -338,24 +342,36 @@ namespace Avalonia.Controls.Primitives { base.OnContainersMaterialized(e); - var selectedIndex = SelectedIndex; - var selectedContainer = e.Containers - .FirstOrDefault(x => (x.ContainerControl as ISelectable)?.IsSelected == true); + var resetSelectedItems = false; - if (selectedContainer != null) + foreach (var container in e.Containers) { - SelectedIndex = selectedContainer.Index; - } - else if (selectedIndex >= e.StartingIndex && - selectedIndex < e.StartingIndex + e.Containers.Count) - { - var container = e.Containers[selectedIndex - e.StartingIndex]; + if ((container.ContainerControl as ISelectable)?.IsSelected == true) + { + if (SelectedIndex == -1) + { + SelectedIndex = container.Index; + } + else + { + if (_selection.Add(container.Index)) + { + resetSelectedItems = true; + } + } - if (container.ContainerControl != null) + MarkContainerSelected(container.ContainerControl, true); + } + else if (_selection.Contains(container.Index)) { MarkContainerSelected(container.ContainerControl, true); } } + + if (resetSelectedItems) + { + ResetSelectedItems(); + } } /// diff --git a/src/Avalonia.Controls/Primitives/TabStrip.cs b/src/Avalonia.Controls/Primitives/TabStrip.cs index 0e15ae4d7b..a61757e628 100644 --- a/src/Avalonia.Controls/Primitives/TabStrip.cs +++ b/src/Avalonia.Controls/Primitives/TabStrip.cs @@ -12,11 +12,8 @@ namespace Avalonia.Controls.Primitives private static readonly FuncTemplate DefaultPanel = new FuncTemplate(() => new WrapPanel { Orientation = Orientation.Horizontal }); - private static IMemberSelector s_MemberSelector = new FuncMemberSelector(SelectHeader); - static TabStrip() { - MemberSelectorProperty.OverrideDefaultValue(s_MemberSelector); SelectionModeProperty.OverrideDefaultValue(SelectionMode.AlwaysSelected); FocusableProperty.OverrideDefaultValue(typeof(TabStrip), false); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); @@ -51,11 +48,5 @@ namespace Avalonia.Controls.Primitives e.Handled = UpdateSelectionFromEventSource(e.Source); } } - - private static object SelectHeader(object o) - { - var headered = o as IHeadered; - return (headered != null) ? (headered.Header ?? string.Empty) : o; - } } } diff --git a/src/Avalonia.Controls/Primitives/TemplatedControl.cs b/src/Avalonia.Controls/Primitives/TemplatedControl.cs index 32e220b789..47c3240374 100644 --- a/src/Avalonia.Controls/Primitives/TemplatedControl.cs +++ b/src/Avalonia.Controls/Primitives/TemplatedControl.cs @@ -257,13 +257,14 @@ namespace Avalonia.Controls.Primitives { Logger.Verbose(LogArea.Control, this, "Creating control template"); - var child = template.Build(this); - var nameScope = new NameScope(); - NameScope.SetNameScope((Control)child, nameScope); + var (child, nameScope) = template.Build(this); ApplyTemplatedParent(child); - RegisterNames(child, nameScope); ((ISetLogicalParent)child).SetParent(this); VisualChildren.Add(child); + + // Existing code kinda expect to see a NameScope even if it's empty + if (nameScope == null) + nameScope = new NameScope(); OnTemplateApplied(new TemplateAppliedEventArgs(nameScope)); } @@ -342,26 +343,5 @@ namespace Avalonia.Controls.Primitives } } } - - /// - /// Registers each control with its name scope. - /// - /// The control. - /// The name scope. - private void RegisterNames(IControl control, INameScope nameScope) - { - if (control.Name != null) - { - nameScope.Register(control.Name, control); - } - - if (control.TemplatedParent == this) - { - foreach (IControl child in control.GetLogicalChildren()) - { - RegisterNames(child, nameScope); - } - } - } } } diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs index 4c3c92309e..19458cc6b8 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate.cs @@ -16,9 +16,15 @@ namespace Avalonia.Controls.Templates /// Initializes a new instance of the class. /// /// The build function. - public FuncControlTemplate(Func build) + public FuncControlTemplate(Func build) : base(build) { } + + public new ControlTemplateResult Build(ITemplatedControl param) + { + var (control, scope) = BuildWithNameScope(param); + return new ControlTemplateResult(control, scope); + } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs index 8e49b51cb8..eec7a6030f 100644 --- a/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs +++ b/src/Avalonia.Controls/Templates/FuncControlTemplate`2.cs @@ -17,9 +17,9 @@ namespace Avalonia.Controls.Templates /// Initializes a new instance of the class. /// /// The build function. - public FuncControlTemplate(Func build) - : base(x => build((T)x)) + public FuncControlTemplate(Func build) + : base((x, s) => build((T)x, s)) { } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs index 1d90fcd01e..204540e431 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls.Templates /// public static readonly FuncDataTemplate Default = new FuncDataTemplate( - data => + (data, s) => { if (data != null) { @@ -49,7 +49,7 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Type type, - Func build, + Func build, bool supportsRecycling = false) : this(o => IsInstance(o, type), build, supportsRecycling) { @@ -67,7 +67,7 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Func match, - Func build, + Func build, bool supportsRecycling = false) : base(build) { @@ -105,4 +105,4 @@ namespace Avalonia.Controls.Templates return (o != null) && t.GetTypeInfo().IsAssignableFrom(o.GetType().GetTypeInfo()); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs index 9339aa6924..68b737928d 100644 --- a/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncDataTemplate`1.cs @@ -18,7 +18,7 @@ namespace Avalonia.Controls.Templates /// A function which when passed an object of returns a control. /// /// Whether the control can be recycled. - public FuncDataTemplate(Func build, bool supportsRecycling = false) + public FuncDataTemplate(Func build, bool supportsRecycling = false) : base(typeof(T), CastBuild(build), supportsRecycling) { } @@ -35,12 +35,30 @@ namespace Avalonia.Controls.Templates /// Whether the control can be recycled. public FuncDataTemplate( Func match, - Func build, + Func build, bool supportsRecycling = false) : base(CastMatch(match), CastBuild(build), supportsRecycling) { } + /// + /// Initializes a new instance of the class. + /// + /// + /// A function which determines whether the data template matches the specified data. + /// + /// + /// A function which when passed an object of returns a control. + /// + /// Whether the control can be recycled. + public FuncDataTemplate( + Func match, + Func build, + bool supportsRecycling = false) + : this(match, (a, _) => build(a), supportsRecycling) + { + } + /// /// Casts a strongly typed match function to a weakly typed one. /// @@ -57,9 +75,9 @@ namespace Avalonia.Controls.Templates /// The strong data type. /// The strongly typed function. /// The weakly typed function. - private static Func CastBuild(Func f) + private static Func CastBuild(Func f) { - return o => f((T)o); + return (o, s) => f((T)o, s); } } } diff --git a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs b/src/Avalonia.Controls/Templates/FuncMemberSelector.cs deleted file mode 100644 index 5ab186261e..0000000000 --- a/src/Avalonia.Controls/Templates/FuncMemberSelector.cs +++ /dev/null @@ -1,35 +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; - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object using a . - /// - public class FuncMemberSelector : IMemberSelector - { - private readonly Func _selector; - - /// - /// Initializes a new instance of the - /// class. - /// - /// The selector. - public FuncMemberSelector(Func selector) - { - this._selector = selector; - } - - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - public object Select(object o) - { - return (o is TObject) ? _selector((TObject)o) : default(TMember); - } - } -} diff --git a/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs new file mode 100644 index 0000000000..76bc298e30 --- /dev/null +++ b/src/Avalonia.Controls/Templates/FuncTemplateNameScopeExtensions.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Controls.Templates +{ + public static class FuncTemplateNameScopeExtensions + { + public static T RegisterInNameScope(this T control, INameScope scope) + where T : StyledElement + { + scope.Register(control.Name, control); + return control; + } + } +} diff --git a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs index b1ce63fdf1..e0608d0471 100644 --- a/src/Avalonia.Controls/Templates/FuncTemplate`2.cs +++ b/src/Avalonia.Controls/Templates/FuncTemplate`2.cs @@ -13,13 +13,13 @@ namespace Avalonia.Controls.Templates public class FuncTemplate : ITemplate where TControl : IControl { - private readonly Func _func; + private readonly Func _func; /// /// Initializes a new instance of the class. /// /// The function used to create the control. - public FuncTemplate(Func func) + public FuncTemplate(Func func) { Contract.Requires(func != null); @@ -35,7 +35,14 @@ namespace Avalonia.Controls.Templates /// public TControl Build(TParam param) { - return _func(param); + return BuildWithNameScope(param).control; + } + + protected (TControl control, INameScope nameScope) BuildWithNameScope(TParam param) + { + var scope = new NameScope(); + var rv = _func(param, scope); + return (rv, scope); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs index e7c9cf8608..998ef0e9f2 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate.cs @@ -28,7 +28,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Type type, - Func build, + Func build, Func itemsSelector) : this(o => IsInstance(o, type), build, itemsSelector) { @@ -48,7 +48,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Func match, - Func build, + Func build, Func itemsSelector) : base(match, build) { diff --git a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs index 4ca96f60bd..41f870ab42 100644 --- a/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs +++ b/src/Avalonia.Controls/Templates/FuncTreeDataTemplate`1.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.Templates /// items. /// public FuncTreeDataTemplate( - Func build, + Func build, Func itemsSelector) : base( typeof(T), @@ -46,7 +46,7 @@ namespace Avalonia.Controls.Templates /// public FuncTreeDataTemplate( Func match, - Func build, + Func build, Func itemsSelector) : base( CastMatch(match), @@ -65,6 +65,17 @@ namespace Avalonia.Controls.Templates return o => (o is T) && f((T)o); } + /// + /// Casts a function with a typed parameter to an untyped function. + /// + /// The result. + /// The typed function. + /// The untyped function. + private static Func Cast(Func f) + { + return (o, s) => f((T)o, s); + } + /// /// Casts a function with a typed parameter to an untyped function. /// diff --git a/src/Avalonia.Controls/Templates/IControlTemplate.cs b/src/Avalonia.Controls/Templates/IControlTemplate.cs index 408329c450..a3ef0fa954 100644 --- a/src/Avalonia.Controls/Templates/IControlTemplate.cs +++ b/src/Avalonia.Controls/Templates/IControlTemplate.cs @@ -9,7 +9,25 @@ namespace Avalonia.Controls.Templates /// /// Interface representing a template used to build a . /// - public interface IControlTemplate : ITemplate + public interface IControlTemplate : ITemplate { } -} \ No newline at end of file + + public class ControlTemplateResult + { + public IControl Control { get; } + public INameScope NameScope { get; } + + public ControlTemplateResult(IControl control, INameScope nameScope) + { + Control = control; + NameScope = nameScope; + } + + public void Deconstruct(out IControl control, out INameScope scope) + { + control = Control; + scope = NameScope; + } + } +} diff --git a/src/Avalonia.Controls/Templates/IMemberSelector.cs b/src/Avalonia.Controls/Templates/IMemberSelector.cs deleted file mode 100644 index e1ec42a849..0000000000 --- a/src/Avalonia.Controls/Templates/IMemberSelector.cs +++ /dev/null @@ -1,18 +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. - -namespace Avalonia.Controls.Templates -{ - /// - /// Selects a member of an object. - /// - public interface IMemberSelector - { - /// - /// Selects a member of an object. - /// - /// The object. - /// The selected member. - object Select(object o); - } -} diff --git a/src/Avalonia.Controls/Templates/ITemplate`2.cs b/src/Avalonia.Controls/Templates/ITemplate`2.cs index f61292c64b..a0c0e88eca 100644 --- a/src/Avalonia.Controls/Templates/ITemplate`2.cs +++ b/src/Avalonia.Controls/Templates/ITemplate`2.cs @@ -8,7 +8,7 @@ namespace Avalonia.Controls.Templates /// /// The type of the parameter. /// The type of control. - public interface ITemplate where TControl : IControl + public interface ITemplate { /// /// Creates the control. @@ -19,4 +19,4 @@ namespace Avalonia.Controls.Templates /// TControl Build(TParam param); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/UserControl.cs b/src/Avalonia.Controls/UserControl.cs index e42ca5e0e6..4f15839821 100644 --- a/src/Avalonia.Controls/UserControl.cs +++ b/src/Avalonia.Controls/UserControl.cs @@ -9,40 +9,8 @@ namespace Avalonia.Controls /// /// Provides the base class for defining a new control that encapsulates related existing controls and provides its own logic. /// - public class UserControl : ContentControl, IStyleable, INameScope + public class UserControl : ContentControl, IStyleable { - private readonly NameScope _nameScope = new NameScope(); - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 5c117f508b..d2793fe0dd 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -48,7 +48,7 @@ namespace Avalonia.Controls /// /// A top-level window. /// - public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot, INameScope + public class Window : WindowBase, IStyleable, IFocusScope, ILayoutRoot { /// /// Defines the property. @@ -157,20 +157,6 @@ namespace Avalonia.Controls _maxPlatformClientSize = PlatformImpl?.MaxClientSize ?? default(Size); } - /// - event EventHandler INameScope.Registered - { - add { _nameScope.Registered += value; } - remove { _nameScope.Registered -= value; } - } - - /// - event EventHandler INameScope.Unregistered - { - add { _nameScope.Unregistered += value; } - remove { _nameScope.Unregistered -= value; } - } - /// /// Gets the platform-specific window implementation. /// @@ -494,24 +480,6 @@ namespace Avalonia.Controls } } - /// - void INameScope.Register(string name, object element) - { - _nameScope.Register(name, element); - } - - /// - object INameScope.Find(string name) - { - return _nameScope.Find(name); - } - - /// - void INameScope.Unregister(string name) - { - _nameScope.Unregister(name); - } - /// protected override Size MeasureOverride(Size availableSize) { diff --git a/src/Avalonia.OpenGL/GlInterface.cs b/src/Avalonia.OpenGL/GlInterface.cs index 718afc4a94..f556949cfa 100644 --- a/src/Avalonia.OpenGL/GlInterface.cs +++ b/src/Avalonia.OpenGL/GlInterface.cs @@ -9,12 +9,14 @@ namespace Avalonia.OpenGL public class GlInterface : GlInterfaceBase { public string Version { get; } + public string Vendor { get; } + public string Renderer { get; } public GlInterface(Func getProcAddress) : base(getProcAddress) { - var versionPtr = GetString(GlConsts.GL_VERSION); - if (versionPtr != IntPtr.Zero) - Version = Marshal.PtrToStringAnsi(versionPtr); + Version = GetString(GlConsts.GL_VERSION); + Renderer = GetString(GlConsts.GL_RENDERER); + Vendor = GetString(GlConsts.GL_VENDOR); } public GlInterface(Func n) : this(ConvertNative(n)) @@ -54,7 +56,15 @@ namespace Avalonia.OpenGL public delegate IntPtr GlGetString(int v); [GlEntryPoint("glGetString")] - public GlGetString GetString { get; } + public GlGetString GetStringNative { get; } + + public string GetString(int v) + { + var ptr = GetStringNative(v); + if (ptr != IntPtr.Zero) + return Marshal.PtrToStringAnsi(ptr); + return null; + } public delegate void GlGetIntegerv(int name, out int rv); [GlEntryPoint("glGetIntegerv")] diff --git a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs index 3f41f54363..4881c77034 100644 --- a/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs +++ b/src/Avalonia.ReactiveUI/AutoDataTemplateBindingHook.cs @@ -16,7 +16,7 @@ namespace Avalonia.ReactiveUI /// public class AutoDataTemplateBindingHook : IPropertyBindingHook { - private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate(x => + private static FuncDataTemplate DefaultItemTemplate = new FuncDataTemplate((x, _) => { var control = new ViewModelViewHost(); var context = control.GetObservable(Control.DataContextProperty); @@ -52,4 +52,4 @@ namespace Avalonia.ReactiveUI return true; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Styling/Controls/ChildNameScope.cs b/src/Avalonia.Styling/Controls/ChildNameScope.cs new file mode 100644 index 0000000000..e6707e71db --- /dev/null +++ b/src/Avalonia.Styling/Controls/ChildNameScope.cs @@ -0,0 +1,73 @@ +using System.Threading.Tasks; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + public class ChildNameScope : INameScope + { + private readonly INameScope _parentScope; + private readonly NameScope _inner = new NameScope(); + + public ChildNameScope(INameScope parentScope) + { + _parentScope = parentScope; + } + + public void Register(string name, object element) => _inner.Register(name, element); + + public SynchronousCompletionAsyncResult FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new SynchronousCompletionAsyncResult(found); + // Not found and both current and parent scope are in completed state + if(IsCompleted) + return new SynchronousCompletionAsyncResult(null); + return DoFindAsync(name); + } + + public SynchronousCompletionAsyncResult DoFindAsync(string name) + { + var src = new SynchronousCompletionAsyncResultSource(); + + void ParentSearch() + { + var parentSearch = _parentScope.FindAsync(name); + if (parentSearch.IsCompleted) + src.SetResult(parentSearch.GetResult()); + else + parentSearch.OnCompleted(() => src.SetResult(parentSearch.GetResult())); + } + if (!_inner.IsCompleted) + { + // Guaranteed to be incomplete at this point + var innerSearch = _inner.FindAsync(name); + innerSearch.OnCompleted(() => + { + var value = innerSearch.GetResult(); + if (value != null) + src.SetResult(value); + else ParentSearch(); + }); + } + else + ParentSearch(); + + return src.AsyncResult; + } + + public object Find(string name) + { + var found = _inner.Find(name); + if (found != null) + return found; + if (_inner.IsCompleted) + return _parentScope.Find(name); + return null; + } + + public void Complete() => _inner.Complete(); + + public bool IsCompleted => _inner.IsCompleted && _parentScope.IsCompleted; + } +} diff --git a/src/Avalonia.Styling/Controls/INameScope.cs b/src/Avalonia.Styling/Controls/INameScope.cs index e4ff90c573..b4f4c8e57b 100644 --- a/src/Avalonia.Styling/Controls/INameScope.cs +++ b/src/Avalonia.Styling/Controls/INameScope.cs @@ -2,6 +2,8 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Threading.Tasks; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -10,16 +12,6 @@ namespace Avalonia.Controls /// public interface INameScope { - /// - /// Raised when an element is registered with the name scope. - /// - event EventHandler Registered; - - /// - /// Raised when an element is unregistered with the name scope. - /// - event EventHandler Unregistered; - /// /// Registers an element in the name scope. /// @@ -28,16 +20,30 @@ namespace Avalonia.Controls void Register(string name, object element); /// - /// Finds a named element in the name scope. + /// Finds a named element in the name scope, waits for the scope to be completely populated before returning null + /// Returned task is configured to run any continuations synchronously. + /// + /// The name. + /// The element, or null if the name was not found. + SynchronousCompletionAsyncResult FindAsync(string name); + + /// + /// Finds a named element in the name scope, returns immediately, doesn't traverse the name scope stack /// /// The name. /// The element, or null if the name was not found. object Find(string name); /// - /// Unregisters an element with the name scope. + /// Marks the name scope as completed, no further registrations will be allowed /// - /// The name. - void Unregister(string name); + void Complete(); + + /// + /// Returns whether further registrations are allowed on the scope + /// + bool IsCompleted { get; } + + } } diff --git a/src/Avalonia.Styling/Controls/NameScope.cs b/src/Avalonia.Styling/Controls/NameScope.cs index e3a29af541..ec01a53cfd 100644 --- a/src/Avalonia.Styling/Controls/NameScope.cs +++ b/src/Avalonia.Styling/Controls/NameScope.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Avalonia.LogicalTree; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -18,44 +20,14 @@ namespace Avalonia.Controls public static readonly AttachedProperty NameScopeProperty = AvaloniaProperty.RegisterAttached("NameScope"); + /// + public bool IsCompleted { get; private set; } + private readonly Dictionary _inner = new Dictionary(); - /// - /// Raised when an element is registered with the name scope. - /// - public event EventHandler Registered; - - /// - /// Raised when an element is unregistered with the name scope. - /// - public event EventHandler Unregistered; - - /// - /// Finds the containing name scope for a styled element. - /// - /// The styled element. - /// The containing name scope. - public static INameScope FindNameScope(StyledElement styled) - { - Contract.Requires(styled != null); - - INameScope result; - - while (styled != null) - { - result = styled as INameScope ?? GetNameScope(styled); - - if (result != null) - { - return result; - } - - styled = (styled as ILogical)?.LogicalParent as StyledElement; - } - - return null; - } - + private readonly Dictionary> _pendingSearches = + new Dictionary>(); + /// /// Gets the value of the attached on a styled element. /// @@ -80,13 +52,11 @@ namespace Avalonia.Controls styled.SetValue(NameScopeProperty, value); } - /// - /// Registers an element with the name scope. - /// - /// The element name. - /// The element. + /// public void Register(string name, object element) { + if (IsCompleted) + throw new InvalidOperationException("NameScope is completed, no further registrations are allowed"); Contract.Requires(name != null); Contract.Requires(element != null); @@ -102,15 +72,29 @@ namespace Avalonia.Controls else { _inner.Add(name, element); - Registered?.Invoke(this, new NameScopeEventArgs(name, element)); + if (_pendingSearches.TryGetValue(name, out var tcs)) + { + _pendingSearches.Remove(name); + tcs.SetResult(element); + } } } - /// - /// Finds a named element in the name scope. - /// - /// The name. - /// The element, or null if the name was not found. + public SynchronousCompletionAsyncResult FindAsync(string name) + { + var found = Find(name); + if (found != null) + return new SynchronousCompletionAsyncResult(found); + if (IsCompleted) + return new SynchronousCompletionAsyncResult((object)null); + if (!_pendingSearches.TryGetValue(name, out var tcs)) + // We are intentionally running continuations synchronously here + _pendingSearches[name] = tcs = new SynchronousCompletionAsyncResultSource(); + + return tcs.AsyncResult; + } + + /// public object Find(string name) { Contract.Requires(name != null); @@ -120,21 +104,14 @@ namespace Avalonia.Controls return result; } - /// - /// Unregisters an element with the name scope. - /// - /// The name. - public void Unregister(string name) + public void Complete() { - Contract.Requires(name != null); - - object element; - - if (_inner.TryGetValue(name, out element)) - { - _inner.Remove(name); - Unregistered?.Invoke(this, new NameScopeEventArgs(name, element)); - } + IsCompleted = true; + foreach (var kp in _pendingSearches) + kp.Value.TrySetResult(null); + _pendingSearches.Clear(); } + + } } diff --git a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs index 991a97a614..5921acd3b6 100644 --- a/src/Avalonia.Styling/Controls/NameScopeExtensions.cs +++ b/src/Avalonia.Styling/Controls/NameScopeExtensions.cs @@ -37,6 +37,25 @@ namespace Avalonia.Controls return (T)result; } + /// + /// Finds a named element in an . + /// + /// The element type. + /// The control to take the name scope from. + /// The name. + /// The named element or null if not found. + public static T Find(this ILogical anchor, string name) + where T : class + { + Contract.Requires(anchor != null); + Contract.Requires(name != null); + var styledAnchor = anchor as StyledElement; + if (styledAnchor == null) + return null; + var nameScope = (anchor as INameScope) ?? NameScope.GetNameScope(styledAnchor); + return nameScope?.Find(name); + } + /// /// Gets a named element from an or throws if no element of the /// requested name was found. @@ -67,6 +86,28 @@ namespace Avalonia.Controls return (T)result; } + /// + /// Gets a named element from an or throws if no element of the + /// requested name was found. + /// + /// The element type. + /// The control to take the name scope from. + /// The name. + /// The named element. + public static T Get(this ILogical anchor, string name) + where T : class + { + Contract.Requires(anchor != null); + Contract.Requires(name != null); + + var nameScope = (anchor as INameScope) ?? NameScope.GetNameScope((StyledElement)anchor); + if (nameScope == null) + throw new InvalidOperationException( + "The control doesn't have an associated name scope, probably no registrations has been done yet"); + + return nameScope.Get(name); + } + public static INameScope FindNameScope(this ILogical control) { Contract.Requires(control != null); diff --git a/src/Avalonia.Styling/Controls/NameScopeLocator.cs b/src/Avalonia.Styling/Controls/NameScopeLocator.cs new file mode 100644 index 0000000000..719cf9344b --- /dev/null +++ b/src/Avalonia.Styling/Controls/NameScopeLocator.cs @@ -0,0 +1,64 @@ +using System; +using System.Linq; +using System.Reactive.Disposables; +using System.Reactive.Threading.Tasks; +using System.Reflection; +using System.Threading.Tasks; +using Avalonia.LogicalTree; +using Avalonia.Reactive; +using Avalonia.Utilities; + +namespace Avalonia.Controls +{ + public class NameScopeLocator + { + /// + /// Tracks a named control relative to another control. + /// + /// + /// The control relative from which the other control should be found. + /// + /// The name of the control to find. + public static IObservable Track(INameScope scope, string name) + { + return new NeverEndingSynchronousCompletionAsyncResultObservable(scope.FindAsync(name)); + } + + // This class is implemented in such weird way because for some reason + // our binding system doesn't expect OnCompleted to be ever called and + // seems to treat it as binding cancellation or something + + private class NeverEndingSynchronousCompletionAsyncResultObservable : IObservable + { + private T _value; + private SynchronousCompletionAsyncResult? _asyncResult; + + public NeverEndingSynchronousCompletionAsyncResultObservable(SynchronousCompletionAsyncResult task) + { + if (task.IsCompleted) + _value = task.GetResult(); + else + _asyncResult = task; + } + + public IDisposable Subscribe(IObserver observer) + { + if (_asyncResult?.IsCompleted == true) + { + _value = _asyncResult.Value.GetResult(); + _asyncResult = null; + } + + if (_asyncResult != null) + _asyncResult.Value.OnCompleted(() => + { + observer.OnNext(_asyncResult.Value.GetResult()); + }); + else + observer.OnNext(_value); + + return Disposable.Empty; + } + } + } +} diff --git a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs index 7186143bf9..491f38c153 100644 --- a/src/Avalonia.Styling/LogicalTree/ControlLocator.cs +++ b/src/Avalonia.Styling/LogicalTree/ControlLocator.cs @@ -15,18 +15,6 @@ namespace Avalonia.LogicalTree /// public static class ControlLocator { - /// - /// Tracks a named control relative to another control. - /// - /// - /// The control relative from which the other control should be found. - /// - /// The name of the control to find. - public static IObservable Track(ILogical relativeTo, string name) - { - return new ControlTracker(relativeTo, name); - } - public static IObservable Track(ILogical relativeTo, int ancestorLevel, Type ancestorType = null) { return new ControlTracker(relativeTo, ancestorLevel, ancestorType); @@ -35,18 +23,10 @@ namespace Avalonia.LogicalTree private class ControlTracker : LightweightObservableBase { private readonly ILogical _relativeTo; - private readonly string _name; private readonly int _ancestorLevel; private readonly Type _ancestorType; - INameScope _nameScope; ILogical _value; - public ControlTracker(ILogical relativeTo, string name) - { - _relativeTo = relativeTo; - _name = name; - } - public ControlTracker(ILogical relativeTo, int ancestorLevel, Type ancestorType) { _relativeTo = relativeTo; @@ -66,12 +46,6 @@ namespace Avalonia.LogicalTree _relativeTo.AttachedToLogicalTree -= Attached; _relativeTo.DetachedFromLogicalTree -= Detached; - if (_nameScope != null) - { - _nameScope.Registered -= Registered; - _nameScope.Unregistered -= Unregistered; - } - _value = null; } @@ -88,57 +62,15 @@ namespace Avalonia.LogicalTree private void Detached(object sender, LogicalTreeAttachmentEventArgs e) { - if (_nameScope != null) - { - _nameScope.Registered -= Registered; - _nameScope.Unregistered -= Unregistered; - } - _value = null; PublishNext(null); } - private void Registered(object sender, NameScopeEventArgs e) - { - if (e.Name == _name && e.Element is ILogical logical) - { - _value = logical; - PublishNext(logical); - } - } - - private void Unregistered(object sender, NameScopeEventArgs e) - { - if (e.Name == _name) - { - _value = null; - PublishNext(null); - } - } - private void Update() { - if (_name != null) - { - _nameScope = _relativeTo.FindNameScope(); - - if (_nameScope != null) - { - _nameScope.Registered += Registered; - _nameScope.Unregistered += Unregistered; - _value = _nameScope.Find(_name); - } - else - { - _value = null; - } - } - else - { - _value = _relativeTo.GetLogicalAncestors() - .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) - .ElementAtOrDefault(_ancestorLevel); - } + _value = _relativeTo.GetLogicalAncestors() + .Where(x => _ancestorType?.GetTypeInfo().IsAssignableFrom(x.GetType().GetTypeInfo()) ?? true) + .ElementAtOrDefault(_ancestorLevel); } } } diff --git a/src/Avalonia.Styling/StyledElement.cs b/src/Avalonia.Styling/StyledElement.cs index 146a4c75e7..b4aa89d5bf 100644 --- a/src/Avalonia.Styling/StyledElement.cs +++ b/src/Avalonia.Styling/StyledElement.cs @@ -61,7 +61,6 @@ namespace Avalonia private readonly Classes _classes = new Classes(); private bool _isAttachedToLogicalTree; private IAvaloniaList _logicalChildren; - private INameScope _nameScope; private IResourceDictionary _resources; private Styles _styles; private bool _styled; @@ -82,7 +81,6 @@ namespace Avalonia /// public StyledElement() { - _nameScope = this as INameScope; _isAttachedToLogicalTree = this is IStyleRoot; } @@ -381,7 +379,6 @@ namespace Avalonia { if (_initCount == 0 && (!_styled || force)) { - RegisterWithNameScope(); ApplyStyling(); _styled = true; } @@ -675,19 +672,6 @@ namespace Avalonia AvaloniaLocator.Current.GetService()?.ApplyStyles(this); } - private void RegisterWithNameScope() - { - if (_nameScope == null) - { - _nameScope = NameScope.GetNameScope(this) ?? ((StyledElement)Parent)?._nameScope; - } - - if (Name != null) - { - _nameScope?.Register(Name, this); - } - } - private static void ValidateLogicalChild(ILogical c) { if (c == null) @@ -724,11 +708,6 @@ namespace Avalonia { if (_isAttachedToLogicalTree) { - if (Name != null) - { - _nameScope?.Unregister(Name); - } - _isAttachedToLogicalTree = false; _styleDetach.OnNext(this); OnDetachedFromLogicalTree(e); diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 3702259f35..9312d38c51 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -99,7 +99,6 @@ namespace Avalonia.Styling if (template != null && !isPropertyOfTypeITemplate) { var materialized = template.Build(); - NameScope.SetNameScope((StyledElement)materialized, new NameScope()); value = materialized; } diff --git a/src/Avalonia.Styling/Styling/Styles.cs b/src/Avalonia.Styling/Styling/Styles.cs index 789bb6ffd3..a4563110a9 100644 --- a/src/Avalonia.Styling/Styling/Styles.cs +++ b/src/Avalonia.Styling/Styling/Styles.cs @@ -180,7 +180,7 @@ namespace Avalonia.Styling /// public bool TryGetResource(object key, out object value) { - if (_resources != null && _resources.TryGetValue(key, out value)) + if (_resources != null && _resources.TryGetResource(key, out value)) { return true; } diff --git a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml index 11d8a344d9..788b60892b 100644 --- a/src/Avalonia.Themes.Default/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Default/AutoCompleteBox.xaml @@ -27,7 +27,6 @@ Background="{TemplateBinding Background}" Foreground="{TemplateBinding Foreground}" ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding ValueMemberSelector}" ScrollViewer.HorizontalScrollBarVisibility="Auto" ScrollViewer.VerticalScrollBarVisibility="Auto" /> diff --git a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj index eb2b6789e1..3d525955b4 100644 --- a/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj +++ b/src/Avalonia.Themes.Default/Avalonia.Themes.Default.csproj @@ -18,6 +18,5 @@ - diff --git a/src/Avalonia.Themes.Default/Carousel.xaml b/src/Avalonia.Themes.Default/Carousel.xaml index efe12c4333..955a49a974 100644 --- a/src/Avalonia.Themes.Default/Carousel.xaml +++ b/src/Avalonia.Themes.Default/Carousel.xaml @@ -8,10 +8,9 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" Margin="{TemplateBinding Padding}" - MemberSelector="{TemplateBinding MemberSelector}" SelectedIndex="{TemplateBinding SelectedIndex}" PageTransition="{TemplateBinding PageTransition}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index ca6c2e372e..6227962a48 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -45,7 +45,6 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" - MemberSelector="{TemplateBinding MemberSelector}" VirtualizationMode="{TemplateBinding VirtualizationMode}" /> diff --git a/src/Avalonia.Themes.Default/DataValidationErrors.xaml b/src/Avalonia.Themes.Default/DataValidationErrors.xaml index 0c40a7eb25..f4145a51f5 100644 --- a/src/Avalonia.Themes.Default/DataValidationErrors.xaml +++ b/src/Avalonia.Themes.Default/DataValidationErrors.xaml @@ -29,7 +29,7 @@ - + diff --git a/src/Avalonia.Themes.Default/ItemsControl.xaml b/src/Avalonia.Themes.Default/ItemsControl.xaml index 7b6671b42c..f3def542fc 100644 --- a/src/Avalonia.Themes.Default/ItemsControl.xaml +++ b/src/Avalonia.Themes.Default/ItemsControl.xaml @@ -4,8 +4,7 @@ + ItemTemplate="{TemplateBinding ItemTemplate}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/ListBox.xaml b/src/Avalonia.Themes.Default/ListBox.xaml index 57b0c541b8..59c596bcaa 100644 --- a/src/Avalonia.Themes.Default/ListBox.xaml +++ b/src/Avalonia.Themes.Default/ListBox.xaml @@ -18,10 +18,9 @@ ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" Margin="{TemplateBinding Padding}" - MemberSelector="{TemplateBinding MemberSelector}" VirtualizationMode="{TemplateBinding VirtualizationMode}"/> - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index be86e8b14c..a794d15577 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -56,8 +56,7 @@ Items="{TemplateBinding Items}" ItemsPanel="{TemplateBinding ItemsPanel}" ItemTemplate="{TemplateBinding ItemTemplate}" - Margin="2" - MemberSelector="{TemplateBinding MemberSelector}"/> + Margin="2"/> + Margin="2"/> + ItemTemplate="{TemplateBinding ItemTemplate}"> @@ -18,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Default/TreeView.xaml b/src/Avalonia.Themes.Default/TreeView.xaml index 4e38c6db3a..6ed2fd17b8 100644 --- a/src/Avalonia.Themes.Default/TreeView.xaml +++ b/src/Avalonia.Themes.Default/TreeView.xaml @@ -15,8 +15,7 @@ + Margin="{TemplateBinding Padding}"/> diff --git a/src/Avalonia.Themes.Default/TreeViewItem.xaml b/src/Avalonia.Themes.Default/TreeViewItem.xaml index b5e0e7a005..5dd082cf7a 100644 --- a/src/Avalonia.Themes.Default/TreeViewItem.xaml +++ b/src/Avalonia.Themes.Default/TreeViewItem.xaml @@ -32,8 +32,7 @@ + ItemsPanel="{TemplateBinding ItemsPanel}"/> diff --git a/src/Avalonia.Visuals/Rendering/RenderLayers.cs b/src/Avalonia.Visuals/Rendering/RenderLayers.cs index 0ff7862ab6..e82934fbad 100644 --- a/src/Avalonia.Visuals/Rendering/RenderLayers.cs +++ b/src/Avalonia.Visuals/Rendering/RenderLayers.cs @@ -8,8 +8,8 @@ namespace Avalonia.Rendering { public class RenderLayers : IEnumerable { - private List _inner = new List(); - private Dictionary _index = new Dictionary(); + private readonly List _inner = new List(); + private readonly Dictionary _index = new Dictionary(); public int Count => _inner.Count; public RenderLayer this[IVisual layerRoot] => _index[layerRoot]; @@ -56,6 +56,7 @@ namespace Avalonia.Rendering } _index.Clear(); + _inner.Clear(); } public bool TryGetValue(IVisual layerRoot, out RenderLayer value) diff --git a/src/Avalonia.X11/Glx/GlxDisplay.cs b/src/Avalonia.X11/Glx/GlxDisplay.cs index 5602b33280..04f2a7137c 100644 --- a/src/Avalonia.X11/Glx/GlxDisplay.cs +++ b/src/Avalonia.X11/Glx/GlxDisplay.cs @@ -90,6 +90,19 @@ namespace Avalonia.X11.Glx GlInterface = new GlInterface(GlxInterface.GlxGetProcAddress); if (GlInterface.Version == null) throw new OpenGlException("GL version string is null, aborting"); + if (GlInterface.Renderer == null) + throw new OpenGlException("GL renderer string is null, aborting"); + + if (Environment.GetEnvironmentVariable("AVALONIA_GLX_IGNORE_RENDERER_BLACKLIST") != "1") + { + var blacklist = AvaloniaLocator.Current.GetService() + ?.GlxRendererBlacklist; + if (blacklist != null) + foreach(var item in blacklist) + if (GlInterface.Renderer.Contains(item)) + throw new OpenGlException($"Renderer '{GlInterface.Renderer}' is blacklisted by '{item}'"); + } + } public void ClearContext() => Glx.MakeContextCurrent(_x11.Display, diff --git a/src/Avalonia.X11/X11Platform.cs b/src/Avalonia.X11/X11Platform.cs index cf5902eff7..7bdc61eb28 100644 --- a/src/Avalonia.X11/X11Platform.cs +++ b/src/Avalonia.X11/X11Platform.cs @@ -96,6 +96,14 @@ namespace Avalonia { public bool UseEGL { get; set; } public bool UseGpu { get; set; } = true; + + public List GlxRendererBlacklist { get; set; } = new List + { + // llvmpipe is a software GL rasterizer. If it's returned by glGetString, + // that usually means that something in the system is horribly misconfigured + // and sometimes attempts to use GLX might cause a segfault + "llvmpipe" + }; public string WmClass { get; set; } = Assembly.GetEntryAssembly()?.GetName()?.Name ?? "AvaloniaApplication"; public bool? EnableMultiTouch { get; set; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 6f3dabd568..06c5375520 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -12,7 +12,6 @@ - @@ -33,7 +32,6 @@ - diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs deleted file mode 100644 index 8dc052fe63..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/MemberSelectorTypeConverter.cs +++ /dev/null @@ -1,24 +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.Globalization; -using Avalonia.Markup.Xaml.Templates; - -namespace Avalonia.Markup.Xaml.Converters -{ - using System.ComponentModel; - - public class MemberSelectorTypeConverter : TypeConverter - { - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) - { - return sourceType == typeof(string); - } - - public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) - { - return MemberSelector.Parse((string)value); - } - } -} \ 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 726f4221f8..a466714136 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs @@ -40,7 +40,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions Source = Source, StringFormat = StringFormat, RelativeSource = RelativeSource, - DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)) + DefaultAnchor = new WeakReference(GetDefaultAnchor(descriptorContext)), + NameScope = new WeakReference(serviceProvider.GetService()) }; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs index e28828625f..f46ee8787b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ControlTemplate.cs @@ -17,6 +17,6 @@ namespace Avalonia.Markup.Xaml.Templates public Type TargetType { get; set; } - public IControl Build(ITemplatedControl control) => TemplateContent.Load(Content); + public ControlTemplateResult Build(ITemplatedControl control) => TemplateContent.Load(Content); } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index ec5de8a059..c81a3718a7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -32,6 +32,6 @@ namespace Avalonia.Markup.Xaml.Templates } } - public IControl Build(object data) => TemplateContent.Load(Content); + public IControl Build(object data) => TemplateContent.Load(Content).Control; } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs index 6e4d2a3980..0dd4e57a7f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/ItemsPanelTemplate.cs @@ -14,8 +14,8 @@ namespace Avalonia.Markup.Xaml.Templates public object Content { get; set; } public IPanel Build() - => (IPanel)TemplateContent.Load(Content); + => (IPanel)TemplateContent.Load(Content).Control; object ITemplate.Build() => Build(); } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs deleted file mode 100644 index fa91ab60ff..0000000000 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/MemberSelector.cs +++ /dev/null @@ -1,48 +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 Avalonia.Controls.Templates; -using Avalonia.Data; -using Avalonia.Data.Core; -using Avalonia.Markup.Parsers; -using System; -using System.Reactive.Linq; - -namespace Avalonia.Markup.Xaml.Templates -{ - public class MemberSelector : IMemberSelector - { - private string _memberName; - - public string MemberName - { - get { return _memberName; } - set - { - if (_memberName != value) - { - _memberName = value; - } - } - } - - public static MemberSelector Parse(string s) - { - return new MemberSelector { MemberName = s }; - } - - public object Select(object o) - { - if (string.IsNullOrEmpty(MemberName)) - { - return o; - } - - var expression = ExpressionObserverBuilder.Build(o, MemberName); - object result = AvaloniaProperty.UnsetValue; - - expression.Subscribe(x => result = x); - return (result == AvaloniaProperty.UnsetValue || result is BindingNotification) ? null : result; - } - } -} \ No newline at end of file diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs index b8fec4e8d0..5f36cfe0ac 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/Template.cs @@ -13,8 +13,8 @@ namespace Avalonia.Markup.Xaml.Templates [TemplateContent] public object Content { get; set; } - public IControl Build() => TemplateContent.Load(Content); + public IControl Build() => TemplateContent.Load(Content).Control; object ITemplate.Build() => Build(); } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs index ead373d380..6a30c3861a 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs @@ -4,17 +4,18 @@ using System; using Avalonia.Controls; using System.Collections.Generic; +using Avalonia.Controls.Templates; namespace Avalonia.Markup.Xaml.Templates { public static class TemplateContent { - public static IControl Load(object templateContent) + public static ControlTemplateResult Load(object templateContent) { if (templateContent is Func direct) { - return (IControl)direct(null); + return (ControlTemplateResult)direct(null); } throw new ArgumentException(nameof(templateContent)); } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index bd2b9d2efd..0e2a131afd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -51,9 +51,9 @@ namespace Avalonia.Markup.Xaml.Templates public IControl Build(object data) { - var visualTreeForItem = TemplateContent.Load(Content); + var visualTreeForItem = TemplateContent.Load(Content).Control; visualTreeForItem.DataContext = data; return visualTreeForItem; } } -} \ No newline at end of file +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index 167b75603f..b91d679fba 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -156,7 +156,7 @@ namespace Avalonia.Markup.Xaml.XamlIl { overrideField.SetValue(null, new Action( - target => { populateCb(XamlIlRuntimeHelpers.RootServiceProviderV1, target); })); + target => { populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), target); })); try { return Activator.CreateInstance(targetType); @@ -170,11 +170,11 @@ namespace Avalonia.Markup.Xaml.XamlIl var createCb = Expression.Lambda>( Expression.Convert(Expression.Call( created.GetMethod(AvaloniaXamlIlCompiler.BuildName), isp), typeof(object)), isp).Compile(); - return createCb(XamlIlRuntimeHelpers.RootServiceProviderV1); + return createCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2()); } else { - populateCb(XamlIlRuntimeHelpers.RootServiceProviderV1, rootInstance); + populateCb(XamlIlRuntimeHelpers.CreateRootServiceProviderV2(), rootInstance); return rootInstance; } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index c25e1186d0..63c8b1c074 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -35,6 +35,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"), RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"), + RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject", UriContextProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IUriContext"), ParentStackProvider = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlParentStackProvider"), @@ -53,9 +54,30 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.Emit, }; rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv); + rv.ContextTypeBuilderCallback = (b, c) => EmitNameScopeField(rv, typeSystem, b, c); return rv; } + public const string ContextNameScopeFieldName = "AvaloniaNameScope"; + + private static void EmitNameScopeField(XamlIlLanguageTypeMappings mappings, + IXamlIlTypeSystem typeSystem, + IXamlIlTypeBuilder typebuilder, IXamlIlEmitter constructor) + { + + var nameScopeType = typeSystem.FindType("Avalonia.Controls.INameScope"); + var field = typebuilder.DefineField(nameScopeType, + ContextNameScopeFieldName, true, false); + constructor + .Ldarg_0() + .Ldarg(1) + .Ldtype(nameScopeType) + .EmitCall(mappings.ServiceProvider.GetMethod(new FindMethodMethodSignature("GetService", + typeSystem.FindType("System.Object"), typeSystem.FindType("System.Type")))) + .Stfld(field); + } + + class AttributeResolver : IXamlIlCustomAttributeResolver { private readonly IXamlIlType _typeConverterAttribute; @@ -81,8 +103,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions var ilist = typeSystem.GetType("System.Collections.Generic.IList`1"); AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); - Add("Avalonia.Controls.Templates.IMemberSelector", - "Avalonia.Markup.Xaml.Converters.MemberSelectorTypeConverter"); Add("Avalonia.Controls.WindowIcon","Avalonia.Markup.Xaml.Converters.IconTypeConverter"); Add("System.Globalization.CultureInfo", "System.ComponentModel.CultureInfoConverter"); Add("System.Uri", "Avalonia.Markup.Xaml.Converters.AvaloniaUriTypeConverter"); diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs index 33056fa3e8..805b733feb 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs @@ -15,7 +15,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && pa.Property.DeclaringType.FullName == "Avalonia.StyledElement") { if (context.ParentNodes().FirstOrDefault() is XamlIlManipulationGroupNode mg - && mg.Children.OfType().Any()) + && mg.Children.OfType().Any()) return node; IXamlIlAstValueNode value = null; @@ -41,54 +41,99 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Children = { pa, - new ScopeRegistrationNode(value) + new AvaloniaNameScopeRegistrationXamlIlNode(value, context.GetAvaloniaTypes()) } }; } + if (!context.ParentNodes().Any() + && node is XamlIlValueWithManipulationNode mnode) + { + mnode.Manipulation = new XamlIlManipulationGroupNode(mnode, + new[] + { + mnode.Manipulation, + new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes()) + }); + } return node; } - class ScopeRegistrationNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + class HandleRootObjectScopeNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode { - public IXamlIlAstValueNode Value { get; set; } - public ScopeRegistrationNode(IXamlIlAstValueNode value) : base(value) + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public HandleRootObjectScopeNode(IXamlIlLineInfo lineInfo, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) { - Value = value; + _types = types; } - public override void VisitChildren(IXamlIlAstVisitor visitor) - => Value = (IXamlIlAstValueNode)Value.Visit(visitor); - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) { - var exts = context.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScopeExtensions"); - var findNameScope = exts.FindMethod(m => m.Name == "FindNameScope"); - var registerMethod = findNameScope.ReturnType.FindMethod(m => m.Name == "Register"); - using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) - using (var nameScopeLoc = context.GetLocal(findNameScope.ReturnType)) + var next = codeGen.DefineLabel(); + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + using (var local = codeGen.LocalsPool.GetLocal(_types.StyledElement)) { - var exit = codeGen.DefineLabel(); - codeGen - // var target = {pop} - .Stloc(targetLoc.Local) - // var scope = target.FindNameScope() - .Ldloc(targetLoc.Local) - .Castclass(findNameScope.Parameters[0]) - .EmitCall(findNameScope) - .Stloc(nameScopeLoc.Local) - // if({scope} != null) goto call; - .Ldloc(nameScopeLoc.Local) - .Brfalse(exit) - .Ldloc(nameScopeLoc.Local); - context.Emit(Value, codeGen, Value.Type.GetClrType()); codeGen - .Ldloc(targetLoc.Local) - .EmitCall(registerMethod) - .MarkLabel(exit); + .Isinst(_types.StyledElement) + .Dup() + .Stloc(local.Local) + .Brfalse(next) + .Ldloc(local.Local) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(_types.NameScopeSetNameScope, true) + .MarkLabel(next) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(_types.INameScopeComplete, true); } + return XamlIlNodeEmitResult.Void(1); + } } } + + class AvaloniaNameScopeRegistrationXamlIlNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + public IXamlIlAstValueNode Name { get; set; } + + public AvaloniaNameScopeRegistrationXamlIlNode(IXamlIlAstValueNode name, AvaloniaXamlIlWellKnownTypes types) : base(name) + { + _types = types; + Name = name; + } + + public override void VisitChildren(IXamlIlAstVisitor visitor) + => Name = (IXamlIlAstValueNode)Name.Visit(visitor); + + public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + { + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + + using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) + { + + codeGen + // var target = {pop} + .Stloc(targetLoc.Local) + // _context.NameScope.Register(Name, target) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField); + + context.Emit(Name, codeGen, Name.Type.GetClrType()); + + codeGen + .Ldloc(targetLoc.Local) + .EmitCall(_types.INameScopeRegister, true); + } + + return XamlIlNodeEmitResult.Void(1); + } + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index c054e57380..1efae902c6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -16,13 +16,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers public IXamlIlMethod AvaloniaObjectSetValueMethod { get; } public IXamlIlType IDisposable { get; } public XamlIlTypeWellKnownTypes XamlIlTypes { get; } + public XamlIlLanguageTypeMappings XamlIlMappings { get; } public IXamlIlType Transitions { get; } public IXamlIlType AssignBindingAttribute { get; } public IXamlIlType UnsetValueType { get; } + public IXamlIlType StyledElement { get; } + public IXamlIlType NameScope { get; } + public IXamlIlMethod NameScopeSetNameScope { get; } + public IXamlIlType INameScope { get; } + public IXamlIlMethod INameScopeRegister { get; } + public IXamlIlMethod INameScopeComplete { get; } public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx) { XamlIlTypes = ctx.Configuration.WellKnownTypes; + XamlIlMappings = ctx.Configuration.TypeMappings; AvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObject"); IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject"); AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); @@ -37,8 +45,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers AvaloniaProperty, IBinding, ctx.Configuration.WellKnownTypes.Object); UnsetValueType = ctx.Configuration.TypeSystem.GetType("Avalonia.UnsetValueType"); + StyledElement = ctx.Configuration.TypeSystem.GetType("Avalonia.StyledElement"); + INameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.INameScope"); + INameScopeRegister = INameScope.GetMethod( + new FindMethodMethodSignature("Register", XamlIlTypes.Void, + XamlIlTypes.String, XamlIlTypes.Object) + { + IsStatic = false, DeclaringOnly = true, IsExactMatch = true + }); + INameScopeComplete = INameScope.GetMethod( + new FindMethodMethodSignature("Complete", XamlIlTypes.Void) + { + IsStatic = false, DeclaringOnly = true, IsExactMatch = true + }); + NameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScope"); + NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope", + XamlIlTypes.Void, StyledElement, INameScope) {IsStatic = true}); + AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs index 2d8ea643ac..1c2fa17643 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs @@ -1,8 +1,10 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using Avalonia.Controls; +using Avalonia.Controls.Templates; using Avalonia.Data; // ReSharper disable UnusedMember.Global // ReSharper disable UnusedParameter.Global @@ -17,7 +19,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime var resourceNodes = provider.GetService().Parents .OfType().ToList(); var rootObject = provider.GetService().RootObject; - return sp => builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject)); + var parentScope = provider.GetService(); + return sp => + { + var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope(); + var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope)); + scope.Complete(); + return new ControlTemplateResult((IControl)obj, scope); + }; } class DeferredParentServiceProvider : @@ -27,12 +36,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime { private readonly IServiceProvider _parentProvider; private readonly List _parentResourceNodes; + private readonly INameScope _nameScope; public DeferredParentServiceProvider(IServiceProvider parentProvider, List parentResourceNodes, - object rootObject) + object rootObject, INameScope nameScope) { _parentProvider = parentProvider; _parentResourceNodes = parentResourceNodes; + _nameScope = nameScope; RootObject = rootObject; } @@ -48,6 +59,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime public object GetService(Type serviceType) { + if (serviceType == typeof(INameScope)) + return _nameScope; if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) return this; if (serviceType == typeof(IRootObjectProvider)) @@ -56,6 +69,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime } public object RootObject { get; } + public object IntermediateRootObject => RootObject; } @@ -132,12 +146,28 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime } } - public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(); + [Obsolete("Don't use", true)] + public static readonly IServiceProvider RootServiceProviderV1 = new RootServiceProvider(null); + [DebuggerStepThrough] + public static IServiceProvider CreateRootServiceProviderV2() + { + return new RootServiceProvider(new NameScope()); + } + class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider { + private readonly INameScope _nameScope; + + public RootServiceProvider(INameScope nameScope) + { + _nameScope = nameScope; + } + public object GetService(Type serviceType) { + if (serviceType == typeof(INameScope)) + return _nameScope; if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider)) return this; return null; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 894b2c0282..c2ec091f79 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 894b2c02827fd5eb16a338de5d5b6c9fbc60fef5 +Subproject commit c2ec091f79fb4e1eea629bc823c9c24da7050022 diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs index 06cc85101a..5ef3dc9753 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlTypes.cs @@ -10,7 +10,15 @@ namespace Avalonia.Markup.Xaml public interface IRootObjectProvider { + /// + /// The root object of the xaml file + /// object RootObject { get; } + /// + /// The "current" root object, contains either the root of the xaml file + /// or the root object of the control/data template + /// + object IntermediateRootObject { get; } } public interface IUriContext diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index 0b85e3224d..dbe5800e55 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Reactive; using System.Reactive.Linq; +using Avalonia.Controls; using Avalonia.Data.Converters; using Avalonia.Data.Core; using Avalonia.LogicalTree; @@ -90,6 +91,8 @@ namespace Avalonia.Data public string StringFormat { get; set; } public WeakReference DefaultAnchor { get; set; } + + public WeakReference NameScope { get; set; } /// /// Gets or sets a function used to resolve types from names in the binding path. @@ -110,7 +113,9 @@ namespace Avalonia.Data ExpressionObserver observer; - var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver); + INameScope nameScope = null; + NameScope?.TryGetTarget(out nameScope); + var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope); if (ElementName != null) { @@ -254,9 +259,12 @@ namespace Avalonia.Data ExpressionNode node) { Contract.Requires(target != null); - + + NameScope.TryGetTarget(out var scope); + if (scope == null) + throw new InvalidOperationException("Name scope is null or was already collected"); var result = new ExpressionObserver( - ControlLocator.Track(target, elementName), + NameScopeLocator.Track(scope, elementName), node, null); return result; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index 8cb14277ff..f957bcab1e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -1,5 +1,6 @@ using System; using System.Reactive; +using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.Utilities; @@ -7,7 +8,8 @@ namespace Avalonia.Markup.Parsers { public static class ExpressionObserverBuilder { - internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null) + internal static (ExpressionNode Node, SourceMode Mode) Parse(string expression, bool enableValidation = false, Func typeResolver = null, + INameScope nameScope = null) { if (string.IsNullOrWhiteSpace(expression)) { @@ -15,7 +17,7 @@ namespace Avalonia.Markup.Parsers } var reader = new CharacterReader(expression.AsSpan()); - var parser = new ExpressionParser(enableValidation, typeResolver); + var parser = new ExpressionParser(enableValidation, typeResolver, nameScope); var node = parser.Parse(ref reader); if (!reader.End) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index a1350a8393..ed3e65feda 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -7,6 +7,7 @@ using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Linq; +using Avalonia.Controls; namespace Avalonia.Markup.Parsers { @@ -20,10 +21,12 @@ namespace Avalonia.Markup.Parsers { private readonly bool _enableValidation; private readonly Func _typeResolver; + private readonly INameScope _nameScope; - public ExpressionParser(bool enableValidation, Func typeResolver) + public ExpressionParser(bool enableValidation, Func typeResolver, INameScope nameScope) { _typeResolver = typeResolver; + _nameScope = nameScope; _enableValidation = enableValidation; } @@ -213,7 +216,7 @@ namespace Avalonia.Markup.Parsers throw new ExpressionParseException(r.Position, "Element name expected after '#'."); } - nodes.Add(new ElementNameNode(name.ToString())); + nodes.Add(new ElementNameNode(_nameScope, name.ToString())); return State.AfterMember; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index f09efca7d0..981e93c534 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Controls; using Avalonia.Data.Core; using Avalonia.LogicalTree; @@ -6,11 +7,13 @@ namespace Avalonia.Markup.Parsers.Nodes { internal class ElementNameNode : ExpressionNode { + private readonly WeakReference _nameScope; private readonly string _name; private IDisposable _subscription; - public ElementNameNode(string name) + public ElementNameNode(INameScope nameScope, string name) { + _nameScope = new WeakReference(nameScope); _name = name; } @@ -18,14 +21,10 @@ namespace Avalonia.Markup.Parsers.Nodes protected override void StartListeningCore(WeakReference reference) { - if (reference.Target is ILogical logical) - { - _subscription = ControlLocator.Track(logical, _name).Subscribe(ValueChanged); - } + if (_nameScope.TryGetTarget(out var scope)) + _subscription = NameScopeLocator.Track(scope, _name).Subscribe(ValueChanged); else - { _subscription = null; - } } protected override void StopListeningCore() diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ee4564ff35..262d87d8b6 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -101,6 +101,7 @@ namespace Avalonia.Skia public SKCanvas Canvas { get; } SKCanvas ISkiaDrawingContextImpl.SkCanvas => Canvas; + GRContext ISkiaDrawingContextImpl.GrContext => _grContext; /// public void Clear(Color color) diff --git a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs index ff82990c47..a9b91384d7 100644 --- a/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/ISkiaDrawingContextImpl.cs @@ -6,5 +6,6 @@ namespace Avalonia.Skia public interface ISkiaDrawingContextImpl : IDrawingContextImpl { SKCanvas SkCanvas { get; } + GRContext GrContext { get; } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index bc7fc1c9fa..097aba6dc9 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -608,6 +608,14 @@ namespace Avalonia.Win32.Interop GWL_USERDATA = -21 } + public enum MenuCharParam + { + MNC_IGNORE = 0, + MNC_CLOSE = 1, + MNC_EXECUTE = 2, + MNC_SELECT = 3 + } + [StructLayout(LayoutKind.Sequential)] public struct RGBQUAD { diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 5cc148fa0d..2f7805884d 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -514,6 +514,10 @@ namespace Avalonia.Win32 KeyInterop.KeyFromVirtualKey(ToInt32(wParam)), WindowsKeyboardDevice.Instance.Modifiers); break; + case UnmanagedMethods.WindowsMessage.WM_MENUCHAR: + // mute the system beep + return (IntPtr)((Int32)UnmanagedMethods.MenuCharParam.MNC_CLOSE << 16); + case UnmanagedMethods.WindowsMessage.WM_KEYUP: case UnmanagedMethods.WindowsMessage.WM_SYSKEYUP: e = new RawKeyEventArgs( diff --git a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs index b12b2e3c31..428f878945 100644 --- a/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs +++ b/tests/Avalonia.Base.UnitTests/AvaloniaObjectTests_DataValidation.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Base.UnitTests @@ -34,10 +35,10 @@ namespace Avalonia.Base.UnitTests { var target = new Class1(); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(6)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); - target.SetValue(Class1.ValidatedDirectProperty, new BindingNotification(7)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(6)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.Error)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(new Exception(), BindingErrorType.DataValidationError)); + target.SetValue(Class1.ValidatedDirectIntProperty, new BindingNotification(7)); Assert.Equal( new[] @@ -73,7 +74,7 @@ namespace Avalonia.Base.UnitTests var source = new Subject(); var target = new Class1 { - [!Class1.ValidatedDirectProperty] = source.ToBinding(), + [!Class1.ValidatedDirectIntProperty] = source.ToBinding(), }; source.OnNext(new BindingNotification(6)); @@ -92,6 +93,30 @@ namespace Avalonia.Base.UnitTests target.Notifications.AsEnumerable()); } + [Fact] + public void Bound_Validated_Direct_String_Property_Can_Be_Set_To_Null() + { + var source = new ViewModel + { + StringValue = "foo", + }; + + var target = new Class1 + { + [!Class1.ValidatedDirectStringProperty] = new Binding + { + Path = nameof(ViewModel.StringValue), + Source = source, + }, + }; + + Assert.Equal("foo", target.ValidatedDirectString); + + source.StringValue = null; + + Assert.Null(target.ValidatedDirectString); + } + private class Class1 : AvaloniaObject { public static readonly StyledProperty NonValidatedProperty = @@ -104,15 +129,23 @@ namespace Avalonia.Base.UnitTests o => o.NonValidatedDirect, (o, v) => o.NonValidatedDirect = v); - public static readonly DirectProperty ValidatedDirectProperty = + public static readonly DirectProperty ValidatedDirectIntProperty = AvaloniaProperty.RegisterDirect( - nameof(ValidatedDirect), - o => o.ValidatedDirect, - (o, v) => o.ValidatedDirect = v, + nameof(ValidatedDirectInt), + o => o.ValidatedDirectInt, + (o, v) => o.ValidatedDirectInt = v, + enableDataValidation: true); + + public static readonly DirectProperty ValidatedDirectStringProperty = + AvaloniaProperty.RegisterDirect( + nameof(ValidatedDirectString), + o => o.ValidatedDirectString, + (o, v) => o.ValidatedDirectString = v, enableDataValidation: true); private int _nonValidatedDirect; - private int _direct; + private int _directInt; + private string _directString; public int NonValidated { @@ -122,14 +155,20 @@ namespace Avalonia.Base.UnitTests public int NonValidatedDirect { - get { return _direct; } + get { return _directInt; } set { SetAndRaise(NonValidatedDirectProperty, ref _nonValidatedDirect, value); } } - public int ValidatedDirect + public int ValidatedDirectInt + { + get { return _directInt; } + set { SetAndRaise(ValidatedDirectIntProperty, ref _directInt, value); } + } + + public string ValidatedDirectString { - get { return _direct; } - set { SetAndRaise(ValidatedDirectProperty, ref _direct, value); } + get { return _directString; } + set { SetAndRaise(ValidatedDirectStringProperty, ref _directString, value); } } public IList Notifications { get; } = new List(); @@ -139,5 +178,16 @@ namespace Avalonia.Base.UnitTests Notifications.Add(notification); } } + + public class ViewModel : NotifyingBase + { + private string _stringValue; + + public string StringValue + { + get { return _stringValue; } + set { _stringValue = value; RaisePropertyChanged(); } + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index b10929cbdc..015a122677 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -1012,23 +1012,23 @@ namespace Avalonia.Controls.UnitTests } private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => { var textBox = new TextBox { Name = "PART_TextBox" - }; + }.RegisterInNameScope(scope); var listbox = new ListBox { Name = "PART_SelectingItemsControl" - }; + }.RegisterInNameScope(scope); var popup = new Popup { Name = "PART_Popup" - }; + }.RegisterInNameScope(scope); var panel = new Panel(); panel.Children.Add(textBox); diff --git a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs index 515e18434d..b16ac6bb8e 100644 --- a/tests/Avalonia.Controls.UnitTests/CarouselTests.cs +++ b/tests/Avalonia.Controls.UnitTests/CarouselTests.cs @@ -302,7 +302,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal("FooBar", target.SelectedItem); } - private Control CreateTemplate(Carousel control) + private Control CreateTemplate(Carousel control, INameScope scope) { return new CarouselPresenter { @@ -312,7 +312,7 @@ namespace Avalonia.Controls.UnitTests [~CarouselPresenter.ItemsPanelProperty] = control[~Carousel.ItemsPanelProperty], [~CarouselPresenter.SelectedIndexProperty] = control[~Carousel.SelectedIndexProperty], [~CarouselPresenter.PageTransitionProperty] = control[~Carousel.PageTransitionProperty], - }; + }.RegisterInNameScope(scope); } } } diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 70ec6c1408..599e214b31 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -80,7 +80,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Panel { @@ -94,7 +94,7 @@ namespace Avalonia.Controls.UnitTests new ToggleButton { Name = "toggle", - }, + }.RegisterInNameScope(scope), new Popup { Name = "PART_Popup", @@ -102,8 +102,8 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_ItemsPresenter", [!ItemsPresenter.ItemsProperty] = parent[!ComboBox.ItemsProperty], - } - } + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope) } }; }); diff --git a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs index c17893604c..93355a22f2 100644 --- a/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContentControlTests.cs @@ -126,7 +126,7 @@ namespace Avalonia.Controls.UnitTests var target = new ContentControl { Template = GetTemplate(), - ContentTemplate = new FuncDataTemplate(_ => new Canvas()), + ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()), }; target.Content = "Foo"; @@ -302,8 +302,8 @@ namespace Avalonia.Controls.UnitTests var target = new ContentControl { - Template = new FuncControlTemplate(_ => presenter), - ContentTemplate = new FuncDataTemplate(x => new Canvas()), + Template = new FuncControlTemplate((_, __) => presenter), + ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()), Content = "foo", }; @@ -333,7 +333,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -343,7 +343,7 @@ namespace Avalonia.Controls.UnitTests Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 6482fcb4da..58d205deaa 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -16,6 +16,60 @@ namespace Avalonia.Controls.UnitTests private Mock popupImpl; private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] + public void Opening_Raises_Single_Opened_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + int openedCount = 0; + + sut.MenuOpened += (sender, args) => + { + openedCount++; + }; + + sut.Open(null); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Closing_Raises_Single_Closed_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + sut.Open(null); + + int closedCount = 0; + + sut.MenuClosed += (sender, args) => + { + closedCount++; + }; + + sut.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void Clicking_On_Control_Toggles_ContextMenu() { diff --git a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs index 936d700ad0..7de80f72cc 100644 --- a/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DatePickerTests.cs @@ -93,28 +93,28 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => { var textBox = new TextBox { Name = "PART_TextBox" - }; + }.RegisterInNameScope(scope); var button = new Button { Name = "PART_Button" - }; + }.RegisterInNameScope(scope); var calendar = new Calendar { Name = "PART_Calendar" - }; + }.RegisterInNameScope(scope); var popup = new Popup { Name = "PART_Popup" - }; + }.RegisterInNameScope(scope); var panel = new Panel(); panel.Children.Add(textBox); diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs index 9b4be59647..70410dff0d 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTests.cs @@ -118,7 +118,7 @@ namespace Avalonia.Controls.UnitTests.Generators { var owner = new Decorator(); var target = new ItemContainerGenerator(owner); - var container = (ContentPresenter)target.Materialize(0, "foo", null).ContainerControl; + var container = (ContentPresenter)target.Materialize(0, "foo").ContainerControl; Assert.Equal("foo", container.Content); @@ -135,7 +135,7 @@ namespace Avalonia.Controls.UnitTests.Generators { var owner = new Decorator(); var target = new ItemContainerGenerator(owner, ListBoxItem.ContentProperty, null); - var container = (ListBoxItem)target.Materialize(0, "foo", null).ContainerControl; + var container = (ListBoxItem)target.Materialize(0, "foo").ContainerControl; Assert.Equal("foo", container.Content); @@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests.Generators foreach (var item in items) { - var container = generator.Materialize(index++, item, null); + var container = generator.Materialize(index++, item); result.Add(container); } diff --git a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs index f63c0efbf9..05954cbcd2 100644 --- a/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Generators/ItemContainerGeneratorTypedTests.cs @@ -35,7 +35,7 @@ namespace Avalonia.Controls.UnitTests.Generators foreach (var item in items) { - var container = generator.Materialize(index++, item, null); + var container = generator.Materialize(index++, item); result.Add(container); } diff --git a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs index a1ba608ec6..a790d2fca1 100644 --- a/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs @@ -20,6 +20,7 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Detects_Horizontal_Orientation() { + GridSplitter splitter; var grid = new Grid() { RowDefinitions = new RowDefinitions("*,Auto,*"), @@ -27,7 +28,7 @@ namespace Avalonia.Controls.UnitTests Children = { new Border { [Grid.RowProperty] = 0 }, - new GridSplitter { [Grid.RowProperty] = 1, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.RowProperty] = 1 }), new Border { [Grid.RowProperty] = 2 } } }; @@ -35,12 +36,13 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(grid.FindControl("splitter").Classes, ":horizontal".Equals); + Assert.Contains(splitter.Classes, ":horizontal".Equals); } [Fact] public void Detects_Vertical_Orientation() { + GridSplitter splitter; var grid = new Grid() { ColumnDefinitions = new ColumnDefinitions("*,Auto,*"), @@ -48,7 +50,7 @@ namespace Avalonia.Controls.UnitTests Children = { new Border { [Grid.ColumnProperty] = 0 }, - new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), new Border { [Grid.ColumnProperty] = 2 }, } }; @@ -56,12 +58,13 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(grid.FindControl("splitter").Classes, ":vertical".Equals); + Assert.Contains(splitter.Classes, ":vertical".Equals); } [Fact] public void Detects_With_Both_Auto() { + GridSplitter splitter; var grid = new Grid() { ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"), @@ -69,7 +72,7 @@ namespace Avalonia.Controls.UnitTests Children = { new Border { [Grid.ColumnProperty] = 0 }, - new GridSplitter { [Grid.ColumnProperty] = 1, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}), new Border { [Grid.ColumnProperty] = 2 }, } }; @@ -77,7 +80,7 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - Assert.Contains(grid.FindControl("splitter").Classes, ":vertical".Equals); + Assert.Contains(splitter.Classes, ":vertical".Equals); } [Fact] @@ -129,13 +132,14 @@ namespace Avalonia.Controls.UnitTests [Fact] public void In_First_Position_Doesnt_Throw_Exception() { + GridSplitter splitter; var grid = new Grid() { ColumnDefinitions = new ColumnDefinitions("Auto,*,*"), RowDefinitions = new RowDefinitions("*,*"), Children = { - new GridSplitter { [Grid.ColumnProperty] = 0, Name = "splitter" }, + (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ), new Border { [Grid.ColumnProperty] = 1 }, new Border { [Grid.ColumnProperty] = 2 }, } @@ -144,7 +148,6 @@ namespace Avalonia.Controls.UnitTests var root = new TestRoot { Child = grid }; root.Measure(new Size(100, 300)); root.Arrange(new Rect(0, 0, 100, 300)); - var splitter = grid.FindControl("splitter"); splitter.RaiseEvent(new VectorEventArgs { RoutedEvent = Thumb.DragDeltaEvent, @@ -199,4 +202,4 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star)); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs index 66789ef874..fcf7bf3c8c 100644 --- a/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs +++ b/tests/Avalonia.Controls.UnitTests/HeaderedItemsControlTests .cs @@ -63,7 +63,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -71,7 +71,7 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_HeaderPresenter", [~ContentPresenter.ContentProperty] = parent[~HeaderedItemsControl.HeaderProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs index 3cf886ade4..b2839360ee 100644 --- a/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests var target = new ItemsControl { Template = GetTemplate(), - ItemTemplate = new FuncDataTemplate(_ => new Canvas()), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), }; target.Items = new[] { "Foo" }; @@ -411,7 +411,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, __) => new Button { Content = x }) }, Items = items, }; @@ -430,27 +430,6 @@ namespace Avalonia.Controls.UnitTests dataContexts); } - [Fact] - public void MemberSelector_Should_Select_Member() - { - var target = new ItemsControl - { - Template = GetTemplate(), - Items = new[] { new Item("Foo"), new Item("Bar") }, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var text = target.Presenter.Panel.Children - .Cast() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, text); - } - [Fact] public void Control_Item_Should_Not_Be_NameScope() { @@ -472,29 +451,6 @@ namespace Avalonia.Controls.UnitTests Assert.Null(NameScope.GetNameScope((TextBlock)item)); } - [Fact] - public void DataTemplate_Created_Content_Should_Be_NameScope() - { - var items = new object[] - { - "foo", - }; - - var target = new ItemsControl - { - Template = GetTemplate(), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var container = (ContentPresenter)target.Presenter.Panel.LogicalChildren[0]; - container.UpdateChild(); - - Assert.NotNull(NameScope.GetNameScope((TextBlock)container.Child)); - } - [Fact] public void Focuses_Next_Item_On_Key_Down() { @@ -578,7 +534,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -586,9 +542,8 @@ namespace Avalonia.Controls.UnitTests Child = new ItemsPresenter { Name = "PART_ItemsPresenter", - MemberSelector = parent.MemberSelector, [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 238e214a5d..9a459328aa 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -25,7 +25,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = new[] { "Foo" }, - ItemTemplate = new FuncDataTemplate(_ => new Canvas()), + ItemTemplate = new FuncDataTemplate((_, __) => new Canvas()), }; Prepare(target); @@ -113,7 +113,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, _) => new Button { Content = x }) }, Items = items, }; @@ -138,7 +138,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), SelectedIndex = 0, }; @@ -162,7 +162,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = Enumerable.Range(0, 20).Select(x => $"Item {x}").ToList(), - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectedIndex = 0, }; @@ -181,7 +181,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectedIndex = 0, }; @@ -205,7 +205,7 @@ namespace Avalonia.Controls.UnitTests Template = ListBoxTemplate(), Items = items, SelectionMode = SelectionMode.Toggle, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }) + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }) }; Prepare(target); @@ -239,7 +239,7 @@ namespace Avalonia.Controls.UnitTests { Template = ListBoxTemplate(), Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 11 }) + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 11 }) }; Prepare(target); @@ -287,7 +287,7 @@ namespace Avalonia.Controls.UnitTests target.DataContext = items; target.VirtualizationMode = virtualizationMode; - target.ItemTemplate = new FuncDataTemplate(c => + target.ItemTemplate = new FuncDataTemplate((c, _) => { var tb = new TextBlock() { Height = 10, Width = 30 }; tb.Bind(TextBlock.TextProperty, new Data.Binding()); @@ -334,7 +334,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate ListBoxTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ScrollViewer { Name = "PART_ScrollViewer", @@ -345,24 +345,24 @@ namespace Avalonia.Controls.UnitTests [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), [~ItemsPresenter.ItemsPanelProperty] = parent.GetObservable(ItemsControl.ItemsPanelProperty).ToBinding(), [~ItemsPresenter.VirtualizationModeProperty] = parent.GetObservable(ListBox.VirtualizationModeProperty).ToBinding(), - } - }); + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope)); } private FuncControlTemplate ListBoxItemTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = parent[!ListBoxItem.ContentProperty], [!ContentPresenter.ContentTemplateProperty] = parent[!ListBoxItem.ContentTemplateProperty], - }); + }.RegisterInNameScope(scope)); } private FuncControlTemplate ScrollViewerTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ScrollContentPresenter { Name = "PART_ContentPresenter", @@ -370,7 +370,7 @@ namespace Avalonia.Controls.UnitTests [~~ScrollContentPresenter.ExtentProperty] = parent[~~ScrollViewer.ExtentProperty], [~~ScrollContentPresenter.OffsetProperty] = parent[~~ScrollViewer.OffsetProperty], [~~ScrollContentPresenter.ViewportProperty] = parent[~~ScrollViewer.ViewportProperty], - }); + }.RegisterInNameScope(scope)); } private void Prepare(ListBox target) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs index de34558ad1..2a61ff1566 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests_Single.cs @@ -245,7 +245,7 @@ namespace Avalonia.Controls.UnitTests } } - private Control CreateListBoxTemplate(ITemplatedControl parent) + private Control CreateListBoxTemplate(ITemplatedControl parent, INameScope scope) { return new ScrollViewer { @@ -254,17 +254,18 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent.GetObservable(ItemsControl.ItemsProperty).ToBinding(), - } + }.RegisterInNameScope(scope) }; } - private Control CreateScrollViewerTemplate(ITemplatedControl parent) + private Control CreateScrollViewerTemplate(ITemplatedControl parent, INameScope scope) { return new ScrollContentPresenter { Name = "PART_ContentPresenter", - [~ContentPresenter.ContentProperty] = parent.GetObservable(ContentControl.ContentProperty).ToBinding(), - }; + [~ContentPresenter.ContentProperty] = + parent.GetObservable(ContentControl.ContentProperty).ToBinding(), + }.RegisterInNameScope(scope); } private void ApplyTemplate(ListBox target) diff --git a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs index f06553411c..638443e17f 100644 --- a/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Mixins/ContentControlMixinTests.cs @@ -21,15 +21,16 @@ namespace Avalonia.Controls.UnitTests.Mixins { var target = new TestControl() { - Template = new FuncControlTemplate(_ => new Panel + Template = new FuncControlTemplate((_, scope) => new Panel { Children = { - new ContentPresenter { Name = "Content_1_Presenter" }, - new ContentPresenter { Name = "Content_2_Presenter" } + new ContentPresenter {Name = "Content_1_Presenter"}.RegisterInNameScope(scope), + new ContentPresenter {Name = "Content_2_Presenter"}.RegisterInNameScope(scope) } }) }; + var ex = Record.Exception(() => target.ApplyTemplate()); @@ -43,12 +44,12 @@ namespace Avalonia.Controls.UnitTests.Mixins var p2 = new ContentPresenter { Name = "Content_2_Presenter" }; var target = new TestControl { - Template = new FuncControlTemplate(_ => new Panel + Template = new FuncControlTemplate((_, scope) => new Panel { Children = { - p1, - p2 + p1.RegisterInNameScope(scope), + p2.RegisterInNameScope(scope) } }) }; diff --git a/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs b/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs index 802f7717e8..0fdcd3fd34 100644 --- a/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/NameScopeTests.cs @@ -20,36 +20,146 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void Unregister_Unregisters_Element() + public void Cannot_Register_New_Element_With_Existing_Name() + { + var target = new NameScope(); + + target.Register("foo", new object()); + Assert.Throws(() => target.Register("foo", new object())); + } + + [Fact] + public void Can_Register_Same_Element_More_Than_Once() { var target = new NameScope(); var element = new object(); target.Register("foo", element); - target.Unregister("foo"); + target.Register("foo", element); - Assert.Null(target.Find("foo")); + Assert.Same(element, target.Find("foo")); } [Fact] - public void Cannot_Register_New_Element_With_Existing_Name() + public void Cannot_Register_New_Element_For_Completed_Scope() { var target = new NameScope(); + var element = new object(); - target.Register("foo", new object()); - Assert.Throws(() => target.Register("foo", new object())); + target.Register("foo", element); + target.Complete(); + Assert.Throws(() => target.Register("bar", element)); } + + /* + `async void` here is intentional since we expect the continuation to be + executed *synchronously* and behave more like an event handler to make sure that + that the object graph is completely ready to use after it's built + rather than have pending continuations queued by SynchronizationContext. + */ + object _found = null; + async void FindAsync(INameScope scope, string name) + { + _found = await scope.FindAsync(name); + } + [Fact] - public void Can_Register_Same_Element_More_Than_Once() + public void FindAsync_Should_Find_Controls_Added_Earlier() { - var target = new NameScope(); + var scope = new NameScope(); + var element = new object(); + scope.Register("foo", element); + FindAsync(scope, "foo"); + Assert.Same(_found, element); + } + + [Fact] + public void FindAsync_Should_Find_Controls_Added_Later() + { + var scope = new NameScope(); var element = new object(); + + FindAsync(scope, "foo"); + Assert.Null(_found); + scope.Register("foo", element); + Assert.Same(_found, element); + } + + [Fact] + public void FindAsync_Should_Return_Null_After_Scope_Completion() + { + var scope = new NameScope(); + var element = new object(); + bool finished = false; + async void Find(string name) + { + Assert.Null(await scope.FindAsync(name)); + finished = true; + } + Find("foo"); + Assert.False(finished); + scope.Register("bar", element); + Assert.False(finished); + scope.Complete(); + Assert.True(finished); + } - target.Register("foo", element); - target.Register("foo", element); + [Fact] + public void Child_Scope_Should_Not_Find_Control_In_Parent_Scope_Unless_Completed() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + scope.Register("foo", element); + Assert.Null(childScope.Find("foo")); + childScope.Complete(); + Assert.Same(element, childScope.Find("foo")); + } + + [Fact] + public void Child_Scope_Should_Prefer_Own_Elements() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + var childElement = new object(); + scope.Register("foo", element); + childScope.Register("foo", childElement); + childScope.Complete(); + Assert.Same(childElement, childScope.Find("foo")); + } - Assert.Same(element, target.Find("foo")); + [Fact] + public void Child_Scope_FindAsync_Should_Find_Elements_In_Parent_Scope_When_Child_Is_Completed() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + scope.Register("foo", element); + FindAsync(childScope, "foo"); + Assert.Null(_found); + childScope.Complete(); + Assert.Same(element, _found); + } + + + [Fact] + public void Child_Scope_FindAsync_Should_Prefer_Own_Elements() + { + var scope = new NameScope(); + var childScope = new ChildNameScope(scope); + var element = new object(); + var childElement = new object(); + FindAsync(childScope, "foo"); + scope.Register("foo", element); + Assert.Null(_found); + childScope.Register("foo", childElement); + Assert.Same(childElement, childScope.Find("foo")); + childScope.Complete(); + FindAsync(childScope, "foo"); + Assert.Same(childElement, childScope.Find("foo")); } + } } diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs index 708e934214..7d05547799 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_InTemplate.cs @@ -136,17 +136,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Null(NameScope.GetNameScope((Control)target.Child)); } - [Fact] - public void DataTemplate_Created_Control_Should_Be_NameScope() - { - var (target, _) = CreateTarget(); - - target.Content = "Foo"; - - Assert.IsType(target.Child); - Assert.NotNull(NameScope.GetNameScope((Control)target.Child)); - } - [Fact] public void Assigning_Control_To_Content_Should_Not_Set_DataContext() { @@ -170,7 +159,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { var (target, _) = CreateTarget(); - target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + target.ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()); target.Content = "Foo"; Assert.IsType(target.Child); @@ -184,7 +173,7 @@ namespace Avalonia.Controls.UnitTests.Presenters target.Content = "Foo"; Assert.IsType(target.Child); - target.ContentTemplate = new FuncDataTemplate(_ => new Canvas()); + target.ContentTemplate = new FuncDataTemplate((_, __) => new Canvas()); Assert.IsType(target.Child); target.ContentTemplate = null; @@ -209,7 +198,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public void Recycles_DataTemplate() { var (target, _) = CreateTarget(); - target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + target.DataTemplates.Add(new FuncDataTemplate((_, __) => new Border(), true)); target.Content = "foo"; @@ -239,7 +228,7 @@ namespace Avalonia.Controls.UnitTests.Presenters public void Detects_DataTemplate_Doesnt_Support_Recycling() { var (target, _) = CreateTarget(); - target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), false)); + target.DataTemplates.Add(new FuncDataTemplate((_, __) => new Border(), false)); target.Content = "foo"; @@ -256,7 +245,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var (target, _) = CreateTarget(); target.DataTemplates.Add(new FuncDataTemplate(x => x == "bar", _ => new Canvas(), true)); - target.DataTemplates.Add(new FuncDataTemplate(_ => new Border(), true)); + target.DataTemplates.Add(new FuncDataTemplate((_, __) => new Border(), true)); target.Content = "foo"; @@ -278,8 +267,8 @@ namespace Avalonia.Controls.UnitTests.Presenters }; var (target, host) = CreateTarget(); - host.DataTemplates.Add(new FuncDataTemplate(x => textBlock)); - host.DataTemplates.Add(new FuncDataTemplate(x => new Canvas())); + host.DataTemplates.Add(new FuncDataTemplate((_, __) => textBlock)); + host.DataTemplates.Add(new FuncDataTemplate((_, __) => new Canvas())); target.Content = "foo"; Assert.Same(textBlock, target.Child); @@ -296,11 +285,11 @@ namespace Avalonia.Controls.UnitTests.Presenters { var templatedParent = new ContentControl { - Template = new FuncControlTemplate(x => + Template = new FuncControlTemplate((_, s) => new ContentPresenter { Name = "PART_ContentPresenter", - }), + }.RegisterInNameScope(s)), }; var root = new TestRoot { Child = templatedParent }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs index 2facee16b7..ab75a87110 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Standalone.cs @@ -54,7 +54,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = new ContentPresenter { ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; var parentMock = new Mock(); @@ -93,14 +93,14 @@ namespace Avalonia.Controls.UnitTests.Presenters { var contentControl = new ContentControl { - Template = new FuncControlTemplate(c => new ContentPresenter() + Template = new FuncControlTemplate((c, scope) => new ContentPresenter() { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = c[~ContentControl.ContentProperty], [~ContentPresenter.ContentTemplateProperty] = c[~ContentControl.ContentTemplateProperty] - }), + }.RegisterInNameScope(scope)), ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; var parentMock = new Mock(); @@ -144,7 +144,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = new ContentPresenter { ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; var parentMock = new Mock(); @@ -179,7 +179,7 @@ namespace Avalonia.Controls.UnitTests.Presenters var target = new ContentPresenter { ContentTemplate = - new FuncDataTemplate(t => new ContentControl() { Content = t }, false) + new FuncDataTemplate((t, _) => new ContentControl() { Content = t }, false) }; target.Content = "foo"; @@ -235,8 +235,8 @@ namespace Avalonia.Controls.UnitTests.Presenters { DataTemplates = { - new FuncDataTemplate(x => textBlock), - new FuncDataTemplate(x => new Canvas()), + new FuncDataTemplate((x, _) => textBlock), + new FuncDataTemplate((x, _) => new Canvas()), }, }; diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs index 5268c9ac0d..09970926fa 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ContentPresenterTests_Unrooted.cs @@ -90,7 +90,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { DataTemplates = { - new FuncDataTemplate(x => new Decorator()), + new FuncDataTemplate((x, _) => new Decorator()), }, }; @@ -99,4 +99,4 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.IsType(target.Child); } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index 3d13e4c32f..274ca335d9 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -220,7 +220,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { VirtualizationMode = ItemVirtualizationMode.None, Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), }; target.ApplyTemplate(); @@ -310,46 +310,6 @@ namespace Avalonia.Controls.UnitTests.Presenters Assert.Equal(target.Panel, child); } - [Fact] - public void MemberSelector_Should_Select_Member() - { - var target = new ItemsPresenter - { - Items = new[] { new Item("Foo"), new Item("Bar") }, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - - var text = target.Panel.Children - .Cast() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, text); - } - - [Fact] - public void MemberSelector_Should_Set_DataContext() - { - var items = new[] { new Item("Foo"), new Item("Bar") }; - var target = new ItemsPresenter - { - Items = items, - MemberSelector = new FuncMemberSelector(x => x.Value), - }; - - target.ApplyTemplate(); - - var dataContexts = target.Panel.Children - .Cast() - .Do(x => x.UpdateChild()) - .Select(x => x.DataContext) - .ToList(); - - Assert.Equal(new[] { "Foo", "Bar" }, dataContexts); - } - private class Item { public Item(string value) diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs index 3ea32ed719..269da884ba 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization.cs @@ -309,7 +309,7 @@ namespace Avalonia.Controls.UnitTests.Presenters private static IDataTemplate ItemTemplate() { - return new FuncDataTemplate(x => new Canvas + return new FuncDataTemplate((x, _) => new Canvas { Width = 10, Height = 10, diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs index 97d57e9eb6..7a6cf0fba7 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests_Virtualization_Simple.cs @@ -470,7 +470,7 @@ namespace Avalonia.Controls.UnitTests.Presenters { VirtualizationMode = ItemVirtualizationMode.None, Items = items, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Height = 10 }), }; target.ApplyTemplate(); @@ -1046,7 +1046,7 @@ namespace Avalonia.Controls.UnitTests.Presenters private static IDataTemplate StringDataTemplate() { - return new FuncDataTemplate(x => new Canvas + return new FuncDataTemplate((x, _) => new Canvas { Width = 10, Height = 10, @@ -1095,12 +1095,12 @@ namespace Avalonia.Controls.UnitTests.Presenters { public TestContainer() { - Template = new FuncControlTemplate(parent => new ContentPresenter + Template = new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], [~ContentPresenter.ContentTemplateProperty] = parent[~ContentControl.ContentTemplateProperty], - }); + }.RegisterInNameScope(scope)); } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index d4b5d01a6b..059146f17d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -134,12 +134,12 @@ namespace Avalonia.Controls.UnitTests.Primitives { var result = new PopupRoot { - Template = new FuncControlTemplate(parent => + Template = new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = parent[!PopupRoot.ContentProperty], - }), + }.RegisterInNameScope(scope)), }; result.ApplyTemplate(); @@ -154,7 +154,7 @@ namespace Avalonia.Controls.UnitTests.Primitives public TemplatedControlWithPopup() { - Template = new FuncControlTemplate(parent => + Template = new FuncControlTemplate((parent, _) => new Popup { [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty], diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index bd11cf2e3c..2e22725125 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Moq; using Avalonia.Controls.Presenters; @@ -185,6 +186,53 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Popup_Open_Should_Raise_Single_Opened_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + + int openedCount = 0; + + target.Opened += (sender, args) => + { + openedCount++; + }; + + target.Open(); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Popup_Close_Should_Raise_Single_Closed_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + target.Open(); + + int closedCount = 0; + + target.Closed += (sender, args) => + { + closedCount++; + }; + + target.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void PopupRoot_Should_Have_Template_Applied() { @@ -315,16 +363,16 @@ namespace Avalonia.Controls.UnitTests.Primitives return result; } - private static IControl PopupRootTemplate(PopupRoot control) + private static IControl PopupRootTemplate(PopupRoot control, INameScope scope) { return new ContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - }; + }.RegisterInNameScope(scope); } - private static IControl PopupContentControlTemplate(PopupContentControl control) + private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) { return new Popup { @@ -333,7 +381,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], } - }; + }.RegisterInNameScope(scope); } private class PopupContentControl : ContentControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs index d913e3e54f..2eeff4cdf9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/RangeBaseTests.cs @@ -111,7 +111,7 @@ namespace Avalonia.Controls.UnitTests.Primitives var target = new TestRange() { - Template = new FuncControlTemplate(c => + Template = new FuncControlTemplate((c, scope) => { track = new Track() { @@ -122,7 +122,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Name = "PART_Track", Thumb = new Thumb() - }; + }.RegisterInNameScope(scope); if (useXamlBinding) { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs index 672b5c608b..2372987c4c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/ScrollBarTests.cs @@ -169,7 +169,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.False(target.IsVisible); } - private static Control Template(ScrollBar control) + private static Control Template(ScrollBar control, INameScope scope) { return new Border { @@ -185,11 +185,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = new FuncControlTemplate(ThumbTemplate), }, - }, + }.RegisterInNameScope(scope), }; } - private static Control ThumbTemplate(Thumb control) + private static Control ThumbTemplate(Thumb control, INameScope scope) { return new Border { @@ -197,4 +197,4 @@ namespace Avalonia.Controls.UnitTests.Primitives }; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 75cababf54..bc002174ec 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -896,13 +896,13 @@ namespace Avalonia.Controls.UnitTests.Primitives private FuncControlTemplate Template() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new ItemsPresenter { Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }); + }.RegisterInNameScope(scope)); } private class Item : Control, ISelectable diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs index 88cb726469..8618387150 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_AutoSelect.cs @@ -84,13 +84,13 @@ namespace Avalonia.Controls.UnitTests.Primitives private FuncControlTemplate Template() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new ItemsPresenter { Name = "itemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }); + }.RegisterInNameScope(scope)); } private class TestSelector : SelectingItemsControl diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs index d66acdf9ce..972b933bb9 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests_Multiple.cs @@ -53,7 +53,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Assigning_SelectedItems_Should_Set_SelectedIndex() + public void Assigning_Single_SelectedItems_Should_Set_SelectedIndex() { var target = new TestSelector { @@ -62,9 +62,51 @@ namespace Avalonia.Controls.UnitTests.Primitives }; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); target.SelectedItems = new AvaloniaList("bar"); Assert.Equal(1, target.SelectedIndex); + Assert.Equal(new[] { "bar" }, target.SelectedItems); + Assert.Equal(new[] { 1 }, SelectedContainers(target)); + } + + [Fact] + public void Assigning_Multiple_SelectedItems_Should_Set_SelectedIndex() + { + // Note that we don't need SelectionMode = Multiple here. Multiple selections can always + // be made in code. + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); + } + + [Fact] + public void Selected_Items_Should_Be_Marked_When_Panel_Created_After_SelectedItems_Is_Set() + { + // Issue #2565. + var target = new TestSelector + { + Items = new[] { "foo", "bar", "baz" }, + Template = Template(), + }; + + target.ApplyTemplate(); + target.SelectedItems = new AvaloniaList("foo", "bar", "baz"); + target.Presenter.ApplyTemplate(); + + Assert.Equal(0, target.SelectedIndex); + Assert.Equal(new[] { "foo", "bar", "baz" }, target.SelectedItems); + Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target)); } [Fact] @@ -964,7 +1006,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -988,7 +1030,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -1010,7 +1052,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Template = Template(), Items = new[] { "Foo", "Bar", "Baz" }, - ItemTemplate = new FuncDataTemplate(x => new TextBlock { Width = 20, Height = 10 }), + ItemTemplate = new FuncDataTemplate((x, _) => new TextBlock { Width = 20, Height = 10 }), SelectionMode = SelectionMode.Multiple, }; @@ -1026,6 +1068,31 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(1, target.SelectedItems.Count); } + [Fact] + public void Adding_Selected_ItemContainers_Should_Update_Selection() + { + var items = new AvaloniaList(new[] + { + new ItemContainer(), + new ItemContainer(), + }); + + var target = new TestSelector + { + Items = items, + Template = Template(), + }; + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + items.Add(new ItemContainer { IsSelected = true }); + items.Add(new ItemContainer { IsSelected = true }); + + Assert.Equal(2, target.SelectedIndex); + Assert.Equal(items[2], target.SelectedItem); + Assert.Equal(new[] { items[2], items[3] }, target.SelectedItems); + } + [Fact] public void Shift_Right_Click_Should_Not_Select_Mutiple() { @@ -1075,13 +1142,13 @@ namespace Avalonia.Controls.UnitTests.Primitives private FuncControlTemplate Template() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = control[~ItemsControl.ItemsProperty], [~ItemsPresenter.ItemsPanelProperty] = control[~ItemsControl.ItemsPanelProperty], - }); + }.RegisterInNameScope(scope)); } private class TestSelector : SelectingItemsControl @@ -1118,5 +1185,11 @@ namespace Avalonia.Controls.UnitTests.Primitives public List Items { get; } public List SelectedItems { get; } } + + private class ItemContainer : Control, ISelectable + { + public string Value { get; set; } + public bool IsSelected { get; set; } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs index 55b3d6f756..b4570ec229 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TabStripTests.cs @@ -14,60 +14,6 @@ namespace Avalonia.Controls.UnitTests.Primitives { public class TabStripTests { - [Fact] - public void Header_Of_IHeadered_Items_Should_Be_Used() - { - var items = new[] - { -#pragma warning disable CS0252 // Possible unintended reference comparison; left hand side needs cast - Mock.Of(x => x.Header == "foo"), - Mock.Of(x => x.Header == "bar"), -#pragma warning restore CS0252 // Possible unintended reference comparison; left hand side needs cast - }; - - var target = new TabStrip - { - Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var result = target.GetLogicalChildren() - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "bar" }, result); - } - - [Fact] - public void Data_Of_Non_IHeadered_Items_Should_Be_Used() - { - var items = new[] - { - "foo", - "bar" - }; - - var target = new TabStrip - { - Template = new FuncControlTemplate(CreateTabStripTemplate), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var result = target.GetLogicalChildren() - .OfType() - .Select(x => x.Content) - .ToList(); - - Assert.Equal(new[] { "foo", "bar" }, result); - } - [Fact] public void First_Tab_Should_Be_Selected_By_Default() { @@ -159,14 +105,13 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Same("3rd", ((TabItem)target.SelectedItem).Name); } - private Control CreateTabStripTemplate(TabStrip parent) + private Control CreateTabStripTemplate(TabStrip parent, INameScope scope) { return new ItemsPresenter { Name = "itemsPresenter", [!ItemsPresenter.ItemsProperty] = parent[!ItemsControl.ItemsProperty], - [!ItemsPresenter.MemberSelectorProperty] = parent[!ItemsControl.MemberSelectorProperty], - }; + }.RegisterInNameScope(scope); } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs index 166586ace1..d36d0b609b 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/TemplatedControlTests.cs @@ -23,7 +23,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { bool executed = false; - var template = new FuncControlTemplate(_ => + var template = new FuncControlTemplate((_, __) => { executed = true; return new Control(); @@ -42,7 +42,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { bool executed = false; - var template = new FuncControlTemplate(_ => + var template = new FuncControlTemplate((_, __) => { executed = true; return new Control(); @@ -63,7 +63,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = new Panel { @@ -92,35 +92,12 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Empty(target.GetLogicalChildren()); } - [Fact] - public void Templated_Child_Should_Be_NameScope() - { - var target = new TemplatedControl - { - Template = new FuncControlTemplate(_ => new Decorator - { - Child = new Panel - { - Children = - { - new TextBlock(), - new Border(), - } - } - }), - }; - - target.ApplyTemplate(); - - Assert.NotNull(NameScope.GetNameScope((Control)target.GetVisualChildren().Single())); - } - [Fact] public void Templated_Children_Should_Have_TemplatedParent_Set() { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = new Panel { @@ -149,7 +126,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()) + Template = new FuncControlTemplate((_, __) => new Decorator()) }; target.ApplyTemplate(); @@ -165,14 +142,14 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()) + Template = new FuncControlTemplate((_, __) => new Decorator()) }; target.ApplyTemplate(); var child = (Decorator)target.GetVisualChildren().Single(); - target.Template = new FuncControlTemplate(_ => new Canvas()); + target.Template = new FuncControlTemplate((_, __) => new Canvas()); target.ApplyTemplate(); Assert.Null(child.Parent); @@ -183,7 +160,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TemplatedControl { - Template = new FuncControlTemplate(_ => new ScrollViewer()) + Template = new FuncControlTemplate((_, __) => new ScrollViewer()) }; target.ApplyTemplate(); @@ -203,7 +180,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => + Template = new FuncControlTemplate((_, __) => { return new StackPanel { @@ -232,11 +209,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => + Template = new FuncControlTemplate((_, __) => { return new ContentControl { - Template = new FuncControlTemplate(parent => + Template = new FuncControlTemplate((parent, ___) => { return new Border { @@ -271,47 +248,12 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(target, textBlock.TemplatedParent); } - [Fact] - public void Nested_TemplatedControls_Should_Register_With_Correct_NameScope() - { - var target = new ContentControl - { - Template = new FuncControlTemplate(ScrollingContentControlTemplate), - Content = "foo" - }; - - var root = new TestRoot { Child = target }; - target.ApplyTemplate(); - - var border = target.GetVisualChildren().FirstOrDefault(); - Assert.IsType(border); - - var scrollViewer = border.GetVisualChildren().FirstOrDefault(); - Assert.IsType(scrollViewer); - ((ScrollViewer)scrollViewer).ApplyTemplate(); - - var scrollContentPresenter = scrollViewer.GetVisualChildren().FirstOrDefault(); - Assert.IsType(scrollContentPresenter); - ((ContentPresenter)scrollContentPresenter).UpdateChild(); - - var contentPresenter = scrollContentPresenter.GetVisualChildren().FirstOrDefault(); - Assert.IsType(contentPresenter); - - var borderNs = NameScope.GetNameScope((Control)border); - var scrollContentPresenterNs = NameScope.GetNameScope((Control)scrollContentPresenter); - - Assert.NotNull(borderNs); - Assert.Same(scrollViewer, borderNs.Find("ScrollViewer")); - Assert.Same(contentPresenter, borderNs.Find("PART_ContentPresenter")); - Assert.Same(scrollContentPresenter, scrollContentPresenterNs.Find("PART_ContentPresenter")); - } - [Fact] public void ApplyTemplate_Should_Raise_TemplateApplied() { var target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()) + Template = new FuncControlTemplate((_, __) => new Decorator()) }; var raised = false; @@ -334,7 +276,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { var target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), }) @@ -348,7 +290,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Assert.Equal(target, decorator.TemplatedParent); Assert.Equal(target, border.TemplatedParent); - target.Template = new FuncControlTemplate(_ => new Canvas()); + target.Template = new FuncControlTemplate((_, __) => new Canvas()); // Templated children should not be removed here: the control may be re-added // somewhere with the same template, so they could still be of use. @@ -370,7 +312,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = templateChild, }) @@ -392,7 +334,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator + Template = new FuncControlTemplate((_, __) => new Decorator { Child = templateChild, }) @@ -425,7 +367,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -461,7 +403,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -500,7 +442,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -520,7 +462,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { new Setter( TemplatedControl.TemplateProperty, - new FuncControlTemplate(_ => new Decorator + new FuncControlTemplate((_, __) => new Decorator { Child = new Border(), })) @@ -555,7 +497,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = target = new TestTemplatedControl { - Template = new FuncControlTemplate(_ => new Decorator()), + Template = new FuncControlTemplate((_, __) => new Decorator()), } }; @@ -573,7 +515,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - private static IControl ScrollingContentControlTemplate(ContentControl control) + private static IControl ScrollingContentControlTemplate(ContentControl control, INameScope scope) { return new Border { @@ -585,20 +527,20 @@ namespace Avalonia.Controls.UnitTests.Primitives { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = control[!ContentControl.ContentProperty], - } - } + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope) }; } - private static Control ScrollViewerTemplate(ScrollViewer control) + private static Control ScrollViewerTemplate(ScrollViewer control, INameScope scope) { var result = new ScrollContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], - }; + }.RegisterInNameScope(scope); return result; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index d1385176c5..75a2f4178b 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -64,7 +64,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Vector(10, 10), target.Offset); } - private Control CreateTemplate(ScrollViewer control) + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid { @@ -88,7 +88,7 @@ namespace Avalonia.Controls.UnitTests [~~ScrollContentPresenter.OffsetProperty] = control[~~ScrollViewer.OffsetProperty], [~~ScrollContentPresenter.ViewportProperty] = control[~~ScrollViewer.ViewportProperty], [~ScrollContentPresenter.CanHorizontallyScrollProperty] = control[~ScrollViewer.CanHorizontallyScrollProperty], - }, + }.RegisterInNameScope(scope), new ScrollBar { Name = "horizontalScrollBar", @@ -98,7 +98,7 @@ namespace Avalonia.Controls.UnitTests [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.HorizontalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.HorizontalScrollBarVisibilityProperty], [Grid.RowProperty] = 1, - }, + }.RegisterInNameScope(scope), new ScrollBar { Name = "verticalScrollBar", @@ -108,9 +108,9 @@ namespace Avalonia.Controls.UnitTests [~ScrollBar.ViewportSizeProperty] = control[~ScrollViewer.VerticalScrollBarViewportSizeProperty], [~ScrollBar.VisibilityProperty] = control[~ScrollViewer.VerticalScrollBarVisibilityProperty], [Grid.ColumnProperty] = 1, - }, + }.RegisterInNameScope(scope), }, }; } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index e00084eba4..ee8d9cc62e 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -129,7 +129,7 @@ namespace Avalonia.Controls.UnitTests }, }; - var template = new FuncControlTemplate(x => new Decorator()); + var template = new FuncControlTemplate((x, __) => new Decorator()); using (UnitTestApplication.Start(TestServices.RealStyler)) { @@ -176,7 +176,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, __) => new Button { Content = x }) }, Items = items, }; @@ -273,7 +273,7 @@ namespace Avalonia.Controls.UnitTests TabControl target = new TabControl { Template = TabControlTemplate(), - ContentTemplate = new FuncDataTemplate(x => + ContentTemplate = new FuncDataTemplate((x, _) => new TextBlock { Tag = "bar", Text = x }), Items = new[] { "Foo" }, }; @@ -289,7 +289,7 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate TabControlTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new StackPanel { Children = @@ -299,26 +299,26 @@ namespace Avalonia.Controls.UnitTests Name = "PART_ItemsPresenter", [!TabStrip.ItemsProperty] = parent[!TabControl.ItemsProperty], [!TabStrip.ItemTemplateProperty] = parent[!TabControl.ItemTemplateProperty], - }, + }.RegisterInNameScope(scope), new ContentPresenter { Name = "PART_SelectedContentHost", [!ContentPresenter.ContentProperty] = parent[!TabControl.SelectedContentProperty], [!ContentPresenter.ContentTemplateProperty] = parent[!TabControl.SelectedContentTemplateProperty], - } + }.RegisterInNameScope(scope) } }); } private IControlTemplate TabItemTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = parent[!TabItem.HeaderProperty], [!ContentPresenter.ContentTemplateProperty] = parent[!TabItem.HeaderTemplateProperty] - }); + }.RegisterInNameScope(scope)); } private void ApplyTemplate(TabControl target) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 932aada64e..35f0b39210 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -444,6 +444,22 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_Bound_Text_To_Null_Works() + { + using (UnitTestApplication.Start(Services)) + { + var source = new Class1 { Bar = "bar" }; + var target = new TextBox { DataContext = source }; + + target.Bind(TextBox.TextProperty, new Binding("Bar")); + + Assert.Equal("bar", target.Text); + source.Bar = null; + Assert.Null(target.Text); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), @@ -456,7 +472,7 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new TextPresenter { Name = "PART_TextPresenter", @@ -467,7 +483,7 @@ namespace Avalonia.Controls.UnitTests Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), }, - }); + }.RegisterInNameScope(scope)); } private void RaiseKeyEvent(TextBox textBox, Key key, InputModifiers inputModifiers) @@ -492,12 +508,19 @@ namespace Avalonia.Controls.UnitTests private class Class1 : NotifyingBase { private int _foo; + private string _bar; public int Foo { get { return _foo; } set { _foo = value; RaisePropertyChanged(); } } + + public string Bar + { + get { return _bar; } + set { _bar = value; RaisePropertyChanged(); } + } } } } diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs index 4d79dd557e..4aaf0ab5b7 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests_DataValidation.cs @@ -93,7 +93,7 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTemplate() { - return new FuncControlTemplate(control => + return new FuncControlTemplate((control, scope) => new TextPresenter { Name = "PART_TextPresenter", @@ -104,7 +104,7 @@ namespace Avalonia.Controls.UnitTests Priority = BindingPriority.TemplatedParent, RelativeSource = new RelativeSource(RelativeSourceMode.TemplatedParent), }, - }); + }.RegisterInNameScope(scope)); } private class ExceptionTest diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index f112289460..0ee772425b 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -226,12 +226,12 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate CreateTemplate() { - return new FuncControlTemplate(x => + return new FuncControlTemplate((x, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty], - }); + }.RegisterInNameScope(scope)); } private class TestTopLevel : TopLevel diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index bb8466ea3e..919b600a1c 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -51,7 +51,7 @@ namespace Avalonia.Controls.UnitTests Template = CreateTreeViewTemplate(), Items = CreateTestTreeData(), ItemTemplate = new FuncTreeDataTemplate( - _ => new Canvas(), + (_, __) => new Canvas(), x => x.Children), } }; @@ -476,7 +476,7 @@ namespace Avalonia.Controls.UnitTests DataContext = "Base", DataTemplates = { - new FuncDataTemplate(x => new Button { Content = x }) + new FuncDataTemplate((x, _) => new Button { Content = x }) }, Items = items, }; @@ -514,27 +514,6 @@ namespace Avalonia.Controls.UnitTests Assert.Null(NameScope.GetNameScope((TreeViewItem)item)); } - [Fact] - public void DataTemplate_Created_Item_Should_Be_NameScope() - { - var items = new object[] - { - "foo", - }; - - var target = new TreeView - { - Template = CreateTreeViewTemplate(), - Items = items, - }; - - target.ApplyTemplate(); - target.Presenter.ApplyTemplate(); - - var item = target.Presenter.Panel.LogicalChildren[0]; - Assert.NotNull(NameScope.GetNameScope((TreeViewItem)item)); - } - [Fact] public void Should_React_To_Children_Changing() { @@ -923,16 +902,16 @@ namespace Avalonia.Controls.UnitTests private IControlTemplate CreateTreeViewTemplate() { - return new FuncControlTemplate(parent => new ItemsPresenter + return new FuncControlTemplate((parent, scope) => new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - }); + }.RegisterInNameScope(scope)); } private IControlTemplate CreateTreeViewItemTemplate() { - return new FuncControlTemplate(parent => new Panel + return new FuncControlTemplate((parent, scope) => new Panel { Children = { @@ -940,12 +919,12 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_HeaderPresenter", [~ContentPresenter.ContentProperty] = parent[~TreeViewItem.HeaderProperty], - }, + }.RegisterInNameScope(scope), new ItemsPresenter { Name = "PART_ItemsPresenter", [~ItemsPresenter.ItemsProperty] = parent[~ItemsControl.ItemsProperty], - } + }.RegisterInNameScope(scope) } }); } diff --git a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs index 6da771217c..9d3e568582 100644 --- a/tests/Avalonia.Controls.UnitTests/UserControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/UserControlTests.cs @@ -40,7 +40,7 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate GetTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new Border { @@ -49,7 +49,7 @@ namespace Avalonia.Controls.UnitTests { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], - } + }.RegisterInNameScope(scope) }; }); } diff --git a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs index 641979638a..df522397ee 100644 --- a/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -59,13 +59,13 @@ namespace Avalonia.Controls.UnitTests.Utils private FuncControlTemplate CreateWindowTemplate() { - return new FuncControlTemplate(parent => + return new FuncControlTemplate((parent, scope) => { return new ContentPresenter { Name = "PART_ContentPresenter", [~ContentPresenter.ContentProperty] = parent[~ContentControl.ContentProperty], - }; + }.RegisterInNameScope(scope); }); } } diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 12d29f2e5b..3ee6a50e69 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -253,12 +253,12 @@ namespace Avalonia.Controls.UnitTests private FuncControlTemplate CreateTemplate() { - return new FuncControlTemplate(x => + return new FuncControlTemplate((x, scope) => new ContentPresenter { Name = "PART_ContentPresenter", [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty], - }); + }.RegisterInNameScope(scope)); } private class TestWindowBase : WindowBase diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index cd25b79f1f..a841174d2d 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -67,13 +67,15 @@ namespace Avalonia.LeakTests { Func run = () => { + var scope = new NameScope(); var window = new Window { Content = new Canvas { Name = "foo" - } + }.RegisterInNameScope(scope) }; + NameScope.SetNameScope(window, scope); window.Show(); @@ -84,6 +86,8 @@ namespace Avalonia.LeakTests // Clear the content and ensure the Canvas is removed. window.Content = null; + NameScope.SetNameScope(window, null); + window.LayoutManager.ExecuteLayoutPass(); Assert.Null(window.Presenter.Child); @@ -279,7 +283,7 @@ namespace Avalonia.LeakTests DataTemplates = { new FuncTreeDataTemplate( - x => new TextBlock { Text = x.Name }, + (x, _) => new TextBlock { Text = x.Name }, x => x.Children) }, Items = nodes diff --git a/tests/Avalonia.LeakTests/MemberSelectorTests.cs b/tests/Avalonia.LeakTests/MemberSelectorTests.cs deleted file mode 100644 index ffee18ae0a..0000000000 --- a/tests/Avalonia.LeakTests/MemberSelectorTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using Avalonia.Markup.Xaml.Templates; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using JetBrains.dotMemoryUnit; -using Xunit; -using Xunit.Abstractions; - -namespace Avalonia.LeakTests -{ - [DotMemoryUnit(FailIfRunWithoutSupport = false)] - public class MemberSelectorTests - { - public MemberSelectorTests(ITestOutputHelper atr) - { - DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine); - } - - [Fact] - public void Should_Not_Hold_Reference_To_Object() - { - WeakReference dataRef = null; - - var selector = new MemberSelector() { MemberName = "Child.StringValue" }; - - Action run = () => - { - var data = new Item() - { - Child = new Item() { StringValue = "Value1" } - }; - - Assert.Same("Value1", selector.Select(data)); - - dataRef = new WeakReference(data); - }; - - run(); - - GC.Collect(); - - Assert.False(dataRef.IsAlive); - } - - private class Item - { - public Item Child { get; set; } - public int IntValue { get; set; } - - public string StringValue { get; set; } - } - } -} diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.cs index 61df0bffdf..0019b06911 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests_ElementName.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 Avalonia.Controls; using Avalonia.Data; using Avalonia.Markup.Data; @@ -34,10 +35,13 @@ namespace Avalonia.Markup.UnitTests.Data } }; + root.RegisterChildrenNames(); + var binding = new Binding { ElementName = "source", Path = "Text", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(TextBox.TextProperty, binding); @@ -69,10 +73,12 @@ namespace Avalonia.Markup.UnitTests.Data } } }; + root.RegisterChildrenNames(); var binding = new Binding { ElementName = "source", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(ContentControl.ContentProperty, binding); @@ -99,11 +105,13 @@ namespace Avalonia.Markup.UnitTests.Data } } }; - + root.RegisterChildrenNames(); + var binding = new Binding { ElementName = "source", Path = "Text", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(TextBox.TextProperty, binding); @@ -113,7 +121,7 @@ namespace Avalonia.Markup.UnitTests.Data Name = "source", Text = "foo", }); - + root.RegisterChildrenNames(); Assert.Equal("foo", target.Text); } @@ -136,10 +144,12 @@ namespace Avalonia.Markup.UnitTests.Data } } }; + root.RegisterChildrenNames(); var binding = new Binding { ElementName = "source", + NameScope = new WeakReference(NameScope.GetNameScope(root)) }; target.Bind(ContentControl.ContentProperty, binding); @@ -151,6 +161,7 @@ namespace Avalonia.Markup.UnitTests.Data }; stackPanel.Children.Add(source); + root.RegisterChildrenNames(); Assert.Same(source, target.Content); } diff --git a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs index d3e3ce5507..ee95463f1f 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/MultiBindingTests_Converters.cs @@ -23,9 +23,10 @@ namespace Avalonia.Markup.UnitTests.Data DataContext = new Class1(), }; + var format = "{0:0.0} + {1:00}"; var target = new MultiBinding { - StringFormat = "{0:0.0} + {1:00}", + StringFormat = format, Bindings = { new Binding(nameof(Class1.Foo)), @@ -35,7 +36,7 @@ namespace Avalonia.Markup.UnitTests.Data textBlock.Bind(TextBlock.TextProperty, target); - Assert.Equal("1.0 + 02", textBlock.Text); + Assert.Equal(string.Format(format, 1, 2), textBlock.Text); } [Fact] diff --git a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs index ef3864abd7..51cb75a867 100644 --- a/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Data/TemplateBindingTests.cs @@ -18,7 +18,7 @@ namespace Avalonia.Markup.UnitTests.Data { var source = new Button { - Template = new FuncControlTemplate