diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj index af11622838f..adbb2251c0b 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj +++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj @@ -537,6 +537,7 @@ StaggeredLayoutPage.xaml + TokenizingTextBoxPage.xaml diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/SampleDataType.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/SampleDataType.cs index 79448dbd406..971cd08181e 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/SampleDataType.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/SampleDataType.cs @@ -23,7 +23,7 @@ public class SampleDataType public override string ToString() { - return "Sample Data: " + Text; + return Text; } } -} +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/SampleEmailDataType.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/SampleEmailDataType.cs new file mode 100644 index 00000000000..0822836189d --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/SampleEmailDataType.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Toolkit.Uwp.UI.Controls; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages +{ + /// + /// Sample of strongly-typed email address simulated data for . + /// + public class SampleEmailDataType + { + /// + /// Gets or sets symbol to display. + /// + public Symbol Icon { get; set; } + + /// + /// Gets or sets the first name . + /// + public string FirstName { get; set; } + + /// + /// Gets or sets the family name . + /// + public string FamilyName { get; set; } + + /// + /// Gets the display text. + /// + public string DisplayName + { + get + { + return string.Format("{0} {1}", FirstName, FamilyName); + } + } + + /// + /// Gets the formatted email address + /// + public string EmailAddress + { + get + { + return string.Format("{0} <{1}.{2}@contoso.com>", DisplayName, FirstName, FamilyName); + } + } + + public override string ToString() + { + return EmailAddress; + } + } +} \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxCode.bind index 355663ad50b..107122cb618 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxCode.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxCode.bind @@ -1,25 +1,118 @@ +private async void EmailTokenItemClick(object sender, ItemClickEventArgs e) +{ + MessageDialog md = new MessageDialog($"email address {(e.ClickedItem as SampleEmailDataType)?.EmailAddress} clicked", "Clicked Item"); + await md.ShowAsync(); +} + private void TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { if (args.CheckCurrent() && args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) { - if (string.IsNullOrWhiteSpace(sender.Text)) - { - _ttb.SuggestedItemsSource = Array.Empty(); - } - else + _acv.RefreshFilter(); + } +} + +private void TokenItemCreating(object sender, TokenItemAddingEventArgs e) +{ + // Take the user's text and convert it to our data type (if we have a matching one). + e.Item = this._samples.FirstOrDefault((item) => item.Text.Contains(e.TokenText, System.StringComparison.CurrentCultureIgnoreCase)) ?? + // Otherwise, create a new version of our data type + new SampleDataType() + { + Text = e.TokenText, + Icon = Symbol.OutlineStar + }; +} + +private void TokenItemAdded(TokenizingTextBox sender, object data) +{ + if (data is SampleDataType sample) + { + Debug.WriteLine("Added Token: " + sample.Text); + } + else + { + Debug.WriteLine("Added Token: " + data); + } +} + +private void TokenItemRemoved(TokenizingTextBox sender, TokenItemRemovingEventArgs args) +{ + if (args.Item is SampleDataType sample) + { + Debug.WriteLine("Removed Token: " + sample.Text); + } + else + { + Debug.WriteLine("Removed Token: " + args.Item); + } +} + +private void EmailTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) +{ + if (args.CheckCurrent() && args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + _acvEmail.RefreshFilter(); + } +} + +private void EmailTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args) +{ + // Search our list for a matching person + foreach (var person in _emailSamples) + { + if (args.TokenText.Contains(person.EmailAddress) || + args.TokenText.Contains(person.DisplayName, StringComparison.CurrentCultureIgnoreCase)) { - _ttb.SuggestedItemsSource = _samples.Where((item) => item.Text.Contains(sender.Text, System.StringComparison.CurrentCultureIgnoreCase)).OrderByDescending(item => item.Text); + args.Item = person; + return; } } + + // Otherwise don't create a token. + args.Cancel = true; +} + +private void EmailTokenItemAdded(TokenizingTextBox sender, object args) +{ + if (args is SampleEmailDataType sample) + { + Debug.WriteLine("Added Email: " + sample.DisplayName); + } + else + { + Debug.WriteLine("Added Token: " + args); + } + + _acvEmail.RefreshFilter(); } -private async void TokenItemCreating(object sender, TokenItemCreatingEventArgs e) +private void EmailTokenItemRemoved(TokenizingTextBox sender, object args) { - // Take the user's text and convert it to our data type. - using (e.GetDeferral()) + if (args is SampleEmailDataType sample) { - // Can do an async lookup here as well. + Debug.WriteLine("Removed Email: " + sample.DisplayName); + } + else + { + Debug.WriteLine("Removed Token: " + args); + } - e.Item = _samples.FirstOrDefault((item) => item.Text.Contains(e.TokenText, System.StringComparison.CurrentCultureIgnoreCase)); + _acvEmail.RefreshFilter(); +} + +private void EmailList_ItemClick(object sender, ItemClickEventArgs e) +{ + if (e.ClickedItem != null) + { + _ttbEmail.Items.Add(e.ClickedItem); + _ttbEmail.Text = string.Empty; + _acvEmail.RefreshFilter(); } +} + +private void ClearButtonClick(object sender, RoutedEventArgs e) +{ + _ttbEmail.Items.Clear(); + _acvEmail.RefreshFilter(); } \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml index fc88b4641bf..45fcf4f9078 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml @@ -9,5 +9,6 @@ + - + \ No newline at end of file diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml.cs index 8a9ed8acc9c..110c6518894 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml.cs +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxPage.xaml.cs @@ -4,21 +4,53 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; -using System.Threading.Tasks; +using Microsoft.Toolkit.Uwp.UI; using Microsoft.Toolkit.Uwp.UI.Controls; using Microsoft.Toolkit.Uwp.UI.Extensions; +using Windows.UI.Popups; using Windows.UI.Xaml; using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Input; namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages { public sealed partial class TokenizingTextBoxPage : Page, IXamlRenderListener { - private TokenizingTextBox _ttb; + //// TODO: We should use images here. + private readonly List _emailSamples = new List() + { + new SampleEmailDataType() { FirstName = "Marcus", FamilyName = "Perryman", Icon = Symbol.Account }, + new SampleEmailDataType() { FirstName = "Ian", FamilyName = "Smith", Icon = Symbol.AddFriend }, + new SampleEmailDataType() { FirstName = "Peter", FamilyName = "Strange", Icon = Symbol.Attach }, + new SampleEmailDataType() { FirstName = "Alex", FamilyName = "Wilber", Icon = Symbol.AttachCamera }, + new SampleEmailDataType() { FirstName = "Allan", FamilyName = "Deyoung", Icon = Symbol.Audio }, + new SampleEmailDataType() { FirstName = "Adele", FamilyName = "Vance", Icon = Symbol.BlockContact }, + new SampleEmailDataType() { FirstName = "Grady", FamilyName = "Archie", Icon = Symbol.Calculator }, + new SampleEmailDataType() { FirstName = "Megan", FamilyName = "Bowen", Icon = Symbol.Calendar }, + new SampleEmailDataType() { FirstName = "Ben", FamilyName = "Walters", Icon = Symbol.Camera }, + new SampleEmailDataType() { FirstName = "Debra", FamilyName = "Berger", Icon = Symbol.Contact }, + new SampleEmailDataType() { FirstName = "Emily", FamilyName = "Braun", Icon = Symbol.Favorite }, + new SampleEmailDataType() { FirstName = "Christine", FamilyName = "Cline", Icon = Symbol.Link }, + new SampleEmailDataType() { FirstName = "Enrico", FamilyName = "Catteneo", Icon = Symbol.Mail }, + new SampleEmailDataType() { FirstName = "Davit", FamilyName = "Badalyan", Icon = Symbol.Map }, + new SampleEmailDataType() { FirstName = "Diego", FamilyName = "Siciliani", Icon = Symbol.Phone }, + new SampleEmailDataType() { FirstName = "Raul", FamilyName = "Razo", Icon = Symbol.Pin }, + new SampleEmailDataType() { FirstName = "Miriam", FamilyName = "Graham", Icon = Symbol.Rotate }, + new SampleEmailDataType() { FirstName = "Lynne", FamilyName = "Robbins", Icon = Symbol.RotateCamera }, + new SampleEmailDataType() { FirstName = "Lydia", FamilyName = "Holloway", Icon = Symbol.Send }, + new SampleEmailDataType() { FirstName = "Nestor", FamilyName = "Wilke", Icon = Symbol.Tag }, + new SampleEmailDataType() { FirstName = "Patti", FamilyName = "Fernandez", Icon = Symbol.UnFavorite }, + new SampleEmailDataType() { FirstName = "Pradeep", FamilyName = "Gupta", Icon = Symbol.UnPin }, + new SampleEmailDataType() { FirstName = "Joni", FamilyName = "Sherman", Icon = Symbol.Zoom }, + new SampleEmailDataType() { FirstName = "Isaiah", FamilyName = "Langer", Icon = Symbol.ZoomIn }, + new SampleEmailDataType() { FirstName = "Irvin", FamilyName = "Sayers", Icon = Symbol.ZoomOut }, + }; - private List _samples = new List() + // TODO: Setup ACV for this collection as well. + private readonly List _samples = new List() { new SampleDataType() { Text = "Account", Icon = Symbol.Account }, new SampleDataType() { Text = "Add Friend", Icon = Symbol.AddFriend }, @@ -47,67 +79,157 @@ public sealed partial class TokenizingTextBoxPage : Page, IXamlRenderListener new SampleDataType() { Text = "ZoomOut", Icon = Symbol.ZoomOut }, }; + private TokenizingTextBox _ttb; + private TokenizingTextBox _ttbEmail; + private ListView _ttbEmailSuggestions; + private Button _ttbEmailClear; + + private AdvancedCollectionView _acv; + private AdvancedCollectionView _acvEmail; + + private ObservableCollection _selectedEmails; + public TokenizingTextBoxPage() { InitializeComponent(); + + _acv = new AdvancedCollectionView(_samples, false); + _acvEmail = new AdvancedCollectionView(_emailSamples, false); + + _acv.SortDescriptions.Add(new SortDescription(nameof(SampleDataType.Text), SortDirection.Ascending)); + _acvEmail.SortDescriptions.Add(new SortDescription(nameof(SampleEmailDataType.DisplayName), SortDirection.Ascending)); + + Loaded += (sender, e) => { this.OnXamlRendered(this); }; } public void OnXamlRendered(FrameworkElement control) { + _selectedEmails = new ObservableCollection(); + if (_ttb != null) { _ttb.TokenItemAdded -= TokenItemAdded; - _ttb.TokenItemRemoved -= TokenItemRemoved; + _ttb.TokenItemRemoving -= TokenItemRemoved; _ttb.TextChanged -= TextChanged; - _ttb.TokenItemCreating -= TokenItemCreating; + _ttb.TokenItemAdding -= TokenItemCreating; } if (control.FindChildByName("TokenBox") is TokenizingTextBox ttb) { _ttb = ttb; + ////_ttb.ItemsSource = new ObservableCollection(); // TODO: This shouldn't be required, we should initialize in control constructor??? + _ttb.TokenItemAdded += TokenItemAdded; - _ttb.TokenItemRemoved += TokenItemRemoved; + _ttb.TokenItemRemoving += TokenItemRemoved; _ttb.TextChanged += TextChanged; - _ttb.TokenItemCreating += TokenItemCreating; + _ttb.TokenItemAdding += TokenItemCreating; + + _acv.Filter = item => !_ttb.Items.Contains(item) && (item as SampleDataType).Text.Contains(_ttb.Text, System.StringComparison.CurrentCultureIgnoreCase); + + _ttb.SuggestedItemsSource = _acv; } + + // For the Email Selection control + if (_ttbEmail != null) + { + _ttbEmail.ItemClick -= EmailTokenItemClick; + _ttbEmail.TokenItemAdding -= EmailTokenItemAdding; + _ttbEmail.TokenItemAdded -= EmailTokenItemAdded; + _ttbEmail.TokenItemRemoved -= EmailTokenItemRemoved; + _ttbEmail.TextChanged -= EmailTextChanged; + _ttbEmail.PreviewKeyDown -= EmailPreviewKeyDown; + } + + if (control.FindChildByName("TokenBoxEmail") is TokenizingTextBox ttbEmail) + { + _ttbEmail = ttbEmail; + + _ttbEmail.ItemsSource = _selectedEmails; + + // _ttbEmail.ItemClick += EmailTokenItemClick; + _ttbEmail.TokenItemAdding += EmailTokenItemAdding; + _ttbEmail.TokenItemAdded += EmailTokenItemAdded; + _ttbEmail.TokenItemRemoved += EmailTokenItemRemoved; + _ttbEmail.TextChanged += EmailTextChanged; + _ttbEmail.PreviewKeyDown += EmailPreviewKeyDown; + + _acvEmail.Filter = item => !_ttbEmail.Items.Contains(item) && (item as SampleEmailDataType).DisplayName.Contains(_ttbEmail.Text, System.StringComparison.CurrentCultureIgnoreCase); + } + + if (_ttbEmailSuggestions != null) + { + _ttbEmailSuggestions.ItemClick -= EmailList_ItemClick; + _ttbEmailSuggestions.PreviewKeyDown -= EmailList_PreviewKeyDown; + } + + if (control.FindChildByName("EmailList") is ListView ttbList) + { + _ttbEmailSuggestions = ttbList; + + _ttbEmailSuggestions.ItemClick += EmailList_ItemClick; + _ttbEmailSuggestions.PreviewKeyDown += EmailList_PreviewKeyDown; + + _ttbEmailSuggestions.ItemsSource = _acvEmail; + } + + if (_ttbEmailClear != null) + { + _ttbEmailClear.Click -= ClearButtonClick; + } + + if (control.FindChildByName("ClearButton") is Button btn) + { + _ttbEmailClear = btn; + + _ttbEmailClear.Click += ClearButtonClick; + } + } + + private async void EmailTokenItemClick(object sender, ItemClickEventArgs e) + { + MessageDialog md = new MessageDialog($"email address {(e.ClickedItem as SampleEmailDataType)?.EmailAddress} clicked", "Clicked Item"); + await md.ShowAsync(); } private void TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) { if (args.CheckCurrent() && args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) { - if (string.IsNullOrWhiteSpace(sender.Text)) - { - _ttb.SuggestedItemsSource = Array.Empty(); - } - else - { - _ttb.SuggestedItemsSource = _samples.Where((item) => item.Text.Contains(sender.Text, System.StringComparison.CurrentCultureIgnoreCase)).OrderByDescending(item => item.Text); - } + _acv.RefreshFilter(); } } - private void TokenItemCreating(object sender, TokenItemCreatingEventArgs e) + private void TokenItemCreating(object sender, TokenItemAddingEventArgs e) { - // Take the user's text and convert it to our data type. + // Take the user's text and convert it to our data type (if we have a matching one). e.Item = _samples.FirstOrDefault((item) => item.Text.Contains(e.TokenText, System.StringComparison.CurrentCultureIgnoreCase)); + + // Otherwise, create a new version of our data type + if (e.Item == null) + { + e.Item = new SampleDataType() + { + Text = e.TokenText, + Icon = Symbol.OutlineStar + }; + } } - private void TokenItemAdded(TokenizingTextBox sender, TokenizingTextBoxItem args) + private void TokenItemAdded(TokenizingTextBox sender, object data) { // TODO: Add InApp Notification? - if (args.Content is SampleDataType sample) + if (data is SampleDataType sample) { Debug.WriteLine("Added Token: " + sample.Text); } else { - Debug.WriteLine("Added Token: " + args.Content); + Debug.WriteLine("Added Token: " + data); } } - private void TokenItemRemoved(TokenizingTextBox sender, TokenItemRemovedEventArgs args) + private void TokenItemRemoved(TokenizingTextBox sender, TokenItemRemovingEventArgs args) { if (args.Item is SampleDataType sample) { @@ -118,5 +240,105 @@ private void TokenItemRemoved(TokenizingTextBox sender, TokenItemRemovedEventArg Debug.WriteLine("Removed Token: " + args.Item); } } + + private void EmailTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if (args.CheckCurrent() && args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + _acvEmail.RefreshFilter(); + } + } + + private void EmailTokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args) + { + // Search our list for a matching person + foreach (var person in _emailSamples) + { + if (args.TokenText.Contains(person.EmailAddress) || + args.TokenText.Contains(person.DisplayName, StringComparison.CurrentCultureIgnoreCase)) + { + args.Item = person; + return; + } + } + + // Otherwise don't create a token. + args.Cancel = true; + } + + private void EmailTokenItemAdded(TokenizingTextBox sender, object args) + { + if (args is SampleEmailDataType sample) + { + Debug.WriteLine("Added Email: " + sample.DisplayName); + } + else + { + Debug.WriteLine("Added Token: " + args); + } + + _acvEmail.RefreshFilter(); + } + + private void EmailTokenItemRemoved(TokenizingTextBox sender, object args) + { + if (args is SampleEmailDataType sample) + { + Debug.WriteLine("Removed Email: " + sample.DisplayName); + } + else + { + Debug.WriteLine("Removed Token: " + args); + } + + _acvEmail.RefreshFilter(); + } + + private void EmailList_ItemClick(object sender, ItemClickEventArgs e) + { + // TODO: not sure how this is getting to be null, need to make simple repro and file platform issue? + if (e.ClickedItem != null && e.ClickedItem is SampleEmailDataType email) + { + _ttbEmail.Text = string.Empty; // Clear current text + + _ttbEmail.AddTokenItem(email); // Insert new token with picked item to current text location + + _acvEmail.RefreshFilter(); + + _ttbEmail.Focus(FocusState.Programmatic); // Give focus back to type another filter + } + } + + private void ClearButtonClick(object sender, RoutedEventArgs e) + { + _selectedEmails.Clear(); + + _acvEmail.RefreshFilter(); + } + + // Move to Email Suggest ListView list when we keydown from the TTB + private void EmailPreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Down && _ttbEmailSuggestions != null) + { + e.Handled = true; + + _ttbEmailSuggestions.SelectedIndex = 0; + + _ttbEmailSuggestions.Focus(FocusState.Programmatic); + } + } + + private void EmailList_PreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == Windows.System.VirtualKey.Up && + _ttbEmailSuggestions != null && _ttbEmail != null && + _ttbEmailSuggestions.SelectedIndex == 0) + { + e.Handled = true; + + _ttbEmail.Focus(FocusState.Programmatic); // Give focus back to type another filter + } + } } } diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind index fb664f34e36..fd6626519d3 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/TokenizingTextBox/TokenizingTextBoxXaml.bind @@ -1,27 +1,24 @@  - + + + + + + + @@ -40,5 +37,48 @@ - + + Current Edit: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +