diff options
author | Eric Maupin <ermaup@microsoft.com> | 2018-08-31 20:59:22 +0300 |
---|---|---|
committer | Eric Maupin <ermaup@microsoft.com> | 2018-09-05 00:02:35 +0300 |
commit | 718623c0ef4257404e44673676c8fa69f30c9000 (patch) | |
tree | 4b12a004988ef84bc4a812885e99fe63fbae4e10 | |
parent | 1afb2f28df3a9516c3cd42f6c4889d2122abe454 (diff) |
[Core] Add custom expression autocomplete support
-rw-r--r-- | Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs | 90 | ||||
-rw-r--r-- | Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs | 91 |
2 files changed, 181 insertions, 0 deletions
diff --git a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs index 00d7e3b..c207b4f 100644 --- a/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs +++ b/Xamarin.PropertyEditing.Tests/PropertyViewModelTests.cs @@ -1085,6 +1085,96 @@ namespace Xamarin.PropertyEditing.Tests Assert.That (requested, Is.True, "CreateResourceRequested did not fire"); } + [TestCase (true, true, true)] + [TestCase (false, true, false)] + [TestCase (true, false, false)] + public void AutocompleteEnabled (bool customExpressions, bool hasInterface, bool expected) + { + var target = new object (); + var property = GetPropertyMock (); + var editor = new Mock<IObjectEditor> (); + editor.SetupGet (e => e.Target).Returns (target); + SetupPropertySetAndGet (editor, property.Object); + + if (hasInterface) { + string[] results = { "Foo", "Bar", "Baz" }; + var complete = editor.As<ICompleteValues> (); + complete.Setup (c => c.GetCompletionsAsync (property.Object, It.IsAny<string> (), It.IsAny<CancellationToken> ())) + .ReturnsAsync (results); + } + + var mockProvider = new MockEditorProvider (editor.Object); + var resources = new MockResourceProvider (); + var platform = new TargetPlatform (mockProvider, resources) { + SupportsCustomExpressions = customExpressions + }; + + var vm = GetViewModel (platform, property.Object, new[] { editor.Object }); + Assert.That (vm.SupportsAutocomplete, Is.EqualTo (expected)); + } + + [Test] + public void AutocompleteResults () + { + var target = new object (); + var property = GetPropertyMock (); + var editor = new Mock<IObjectEditor> (); + editor.SetupGet (e => e.Target).Returns (target); + SetupPropertySetAndGet (editor, property.Object); + + string[] results = new[] { "Foo", "Bar", "Baz" }; + var complete = editor.As<ICompleteValues> (); + complete.Setup (c => c.GetCompletionsAsync (property.Object, It.IsAny<string> (), It.IsAny<CancellationToken> ())).ReturnsAsync (results); + + var mockProvider = new MockEditorProvider (editor.Object); + var resources = new MockResourceProvider(); + var platform = new TargetPlatform (mockProvider, resources) { + SupportsCustomExpressions = true + }; + + var vm = GetViewModel (platform, property.Object, new[] { editor.Object }); + Assume.That (vm.SupportsAutocomplete, Is.True); + vm.PreviewCustomExpression = "preview"; + + CollectionAssert.AreEqual (vm.AutocompleteItems, results); + complete.Verify (c => c.GetCompletionsAsync (property.Object, "preview", It.IsAny<CancellationToken> ())); + } + + [Test] + public void AutocompleteCancels () + { + var target = new object (); + var property = GetPropertyMock (); + var editor = new Mock<IObjectEditor> (); + editor.SetupGet (e => e.Target).Returns (target); + SetupPropertySetAndGet (editor, property.Object); + + string[] results = new[] { "Foo", "Bar", "Baz" }; + var tcs = new TaskCompletionSource<IReadOnlyList<string>> (); + + var complete = editor.As<ICompleteValues> (); + complete.Setup (c => c.GetCompletionsAsync (property.Object, It.IsAny<string> (), It.IsAny<CancellationToken> ())) + .Returns<IPropertyInfo,string,CancellationToken> ((a,b,c) => { + c.Register (() => { + tcs.TrySetCanceled (); + }); + return tcs.Task; + }); + + var mockProvider = new MockEditorProvider (editor.Object); + var resources = new MockResourceProvider (); + var platform = new TargetPlatform (mockProvider, resources) { + SupportsCustomExpressions = true + }; + + var vm = GetViewModel (platform, property.Object, new[] { editor.Object }); + Assume.That (vm.SupportsAutocomplete, Is.True); + vm.PreviewCustomExpression = "preview"; + + vm.PreviewCustomExpression = "attempt2"; + Assert.That (tcs.Task.IsCanceled, Is.True); + } + protected TViewModel GetViewModel (IPropertyInfo property, IObjectEditor editor) { return GetViewModel (property, new[] { editor }); diff --git a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs index 4e353c8..605b8c5 100644 --- a/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs +++ b/Xamarin.PropertyEditing/ViewModels/PropertyViewModel.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; +using System.Threading; using System.Threading.Tasks; using System.Windows.Input; @@ -77,6 +78,32 @@ namespace Xamarin.PropertyEditing.ViewModels } } + public bool SupportsAutocomplete + { + get { return this.supportsAutocomplete; } + private set + { + if (this.supportsAutocomplete == value) + return; + + this.supportsAutocomplete = value; + OnPropertyChanged(); + + if (!value) { + this.autocomplete = null; + this.autocompleteCancel?.Cancel(); + this.autocompleteCancel = null; + } + } + } + + public IReadOnlyList<string> AutocompleteItems => this.autocomplete; + + public string PreviewCustomExpression + { + set { UpdateAutocomplete (value); } + } + public string CustomExpression { get { return this.value?.CustomExpression; } @@ -228,6 +255,23 @@ namespace Xamarin.PropertyEditing.ViewModels } } + protected override void OnEditorsChanged (object sender, NotifyCollectionChangedEventArgs e) + { + base.OnEditorsChanged (sender, e); + + if (e.Action == NotifyCollectionChangedAction.Add && SupportsAutocomplete) + return; + + if (TargetPlatform.SupportsCustomExpressions) { + foreach (IObjectEditor editor in Editors) { + if (editor is ICompleteValues) { + SupportsAutocomplete = true; + break; + } + } + } + } + private readonly ICoerce<TValue> coerce; private readonly IValidator<TValue> validator; private readonly ICanNavigateToSource valueNavigator; @@ -235,6 +279,10 @@ namespace Xamarin.PropertyEditing.ViewModels private bool isNullable; private ValueInfo<TValue> value; + private bool supportsAutocomplete; + private ObservableCollectionEx<string> autocomplete; + private CancellationTokenSource autocompleteCancel; + private void SignalValueChange () { OnPropertyChanged (nameof (Value)); @@ -359,6 +407,49 @@ namespace Xamarin.PropertyEditing.ViewModels }); } + private async void UpdateAutocomplete (string value) + { + if (!SupportsAutocomplete) + return; + + if (this.autocomplete == null) { + this.autocomplete = new ObservableCollectionEx<string> (); + OnPropertyChanged (nameof(AutocompleteItems)); + } else { + this.autocompleteCancel.Cancel(); + } + + this.autocompleteCancel = new CancellationTokenSource (); + CancellationToken cancel = this.autocompleteCancel.Token; + + try { + HashSet<string> common = null; + List<Task<IReadOnlyList<string>>> tasks = new List<Task<IReadOnlyList<string>>> (); + + foreach (IObjectEditor editor in Editors) { + if (!(editor is ICompleteValues complete)) + continue; + + tasks.Add (complete.GetCompletionsAsync (Property, value, cancel)); + } + + IReadOnlyList<string> list = null; + do { + Task<IReadOnlyList<string>> results = await Task.WhenAny (tasks); + tasks.Remove (results); + + if (list == null) { + list = results.Result; + common = new HashSet<string> (list); + } else + common.IntersectWith (results.Result); + } while (tasks.Count > 0 && !cancel.IsCancellationRequested); + + this.autocomplete.Reset (list.Where (common.Contains)); + } catch (OperationCanceledException) { + } + } + private static TValue DefaultValue; } |