diff --git a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj index a362deb594a..3f870314b3a 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj +++ b/Microsoft.Toolkit.Uwp.SampleApp/Microsoft.Toolkit.Uwp.SampleApp.csproj @@ -281,6 +281,7 @@ + @@ -506,6 +507,9 @@ FocusBehaviorPage.xaml + + MetadataControlPage.xaml + TilesBrushPage.xaml @@ -966,6 +970,13 @@ Designer + + Designer + + + Designer + MSBuild:Compile + Designer MSBuild:Compile diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControl.png b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControl.png new file mode 100644 index 00000000000..63f5ef802c9 Binary files /dev/null and b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControl.png differ diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlCode.bind b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlCode.bind new file mode 100644 index 00000000000..a0b052eb012 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlCode.bind @@ -0,0 +1,15 @@ + + + + + + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlPage.xaml b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlPage.xaml new file mode 100644 index 00000000000..a43e28233f2 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlPage.xaml @@ -0,0 +1,12 @@ + + + + + + diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlPage.xaml.cs b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlPage.xaml.cs new file mode 100644 index 00000000000..115aec26eba --- /dev/null +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/MetadataControl/MetadataControlPage.xaml.cs @@ -0,0 +1,83 @@ +// 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 System; +using System.Collections.ObjectModel; +using Microsoft.Toolkit.Uwp.SampleApp.Common; +using Microsoft.Toolkit.Uwp.UI; +using Microsoft.Toolkit.Uwp.UI.Controls; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Controls; + +namespace Microsoft.Toolkit.Uwp.SampleApp.SamplePages +{ + /// + /// A page that shows how to use the MetadataControl + /// + public sealed partial class MetadataControlPage : Page, IXamlRenderListener + { + private static readonly string[] Labels = "Lorem ipsum dolor sit amet consectetur adipiscing elit".Split(" "); + + private readonly Random _random; + private readonly ObservableCollection _units; + private readonly DelegateCommand _command; + private MetadataControl _metadataControl; + + public MetadataControlPage() + { + _random = new Random(); + _units = new ObservableCollection(); + _command = new DelegateCommand(OnExecuteCommand); + InitializeComponent(); + Setup(); + } + + public void OnXamlRendered(FrameworkElement control) + { + _metadataControl = control.FindChild("metadataControl") as MetadataControl; + if (_metadataControl != null) + { + _metadataControl.Items = _units; + } + } + + private void Setup() + { + SampleController.Current.RegisterNewCommand("Add label", (sender, args) => + { + _units.Add(new MetadataItem { Label = GetRandomLabel() }); + }); + + SampleController.Current.RegisterNewCommand("Add command", (sender, args) => + { + var label = GetRandomLabel(); + _units.Add(new MetadataItem + { + Label = label, + Command = _command, + CommandParameter = label, + }); + }); + + SampleController.Current.RegisterNewCommand("Clear", (sender, args) => + { + _units.Clear(); + }); + } + + private string GetRandomLabel() => Labels[_random.Next(Labels.Length)]; + + private async void OnExecuteCommand(object obj) + { + var dialog = new ContentDialog + { + Title = "Command invoked", + Content = $"Command parameter: {obj}", + CloseButtonText = "OK" + }; + + await dialog.ShowAsync(); + } + } +} diff --git a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json index 4be7498a341..0b169df7126 100644 --- a/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json +++ b/Microsoft.Toolkit.Uwp.SampleApp/SamplePages/samples.json @@ -133,6 +133,16 @@ "Icon": "/SamplePages/RadialProgressBar/RadialProgressBar.png", "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/RadialProgressBar.md" }, + { + "Name": "MetadataControl", + "Type": "MetadataControlPage", + "Subcategory": "Status and Info", + "About": "The control displays a list of metadata separated by bullets. The entries can either be strings or commands.", + "CodeUrl": "https://github.com/windows-toolkit/WindowsCommunityToolkit/tree/master/Microsoft.Toolkit.Uwp.UI.Controls/MetadataControl", + "XamlCodeFile": "MetadataControlCode.bind", + "Icon": "/SamplePages/MetadataControl/MetadataControl.png", + "DocumentationUrl": "https://raw.githubusercontent.com/MicrosoftDocs/WindowsCommunityToolkitDocs/master/docs/controls/MetadataControl.md" + }, { "Name": "RotatorTile", "Type": "RotatorTilePage", diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataControl.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataControl.cs new file mode 100644 index 00000000000..184ca44ba27 --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataControl.cs @@ -0,0 +1,212 @@ +// 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 System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using Windows.UI.Xaml; +using Windows.UI.Xaml.Automation; +using Windows.UI.Xaml.Automation.Peers; +using Windows.UI.Xaml.Controls; +using Windows.UI.Xaml.Documents; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// Display s separated by bullets. + /// + [TemplatePart(Name = TextContainerPart, Type = typeof(TextBlock))] + public sealed class MetadataControl : Control + { + /// + /// The DP to store the property value. + /// + public static readonly DependencyProperty SeparatorProperty = DependencyProperty.Register( + nameof(Separator), + typeof(string), + typeof(MetadataControl), + new PropertyMetadata(" • ", OnPropertyChanged)); + + /// + /// The DP to store the property value. + /// + public static readonly DependencyProperty AccessibleSeparatorProperty = DependencyProperty.Register( + nameof(AccessibleSeparator), + typeof(string), + typeof(MetadataControl), + new PropertyMetadata(", ", OnPropertyChanged)); + + /// + /// The DP to store the property value. + /// + public static readonly DependencyProperty ItemsProperty = DependencyProperty.Register( + nameof(Items), + typeof(IEnumerable), + typeof(MetadataControl), + new PropertyMetadata(null, OnMetadataItemsChanged)); + + /// + /// The DP to store the TextBlockStyle value. + /// + public static readonly DependencyProperty TextBlockStyleProperty = DependencyProperty.Register( + nameof(TextBlockStyle), + typeof(Style), + typeof(MetadataControl), + new PropertyMetadata(null)); + + private const string TextContainerPart = "TextContainer"; + + private TextBlock _textContainer; + + /// + /// Initializes a new instance of the class. + /// + public MetadataControl() + { + DefaultStyleKey = typeof(MetadataControl); + ActualThemeChanged += OnActualThemeChanged; + } + + /// + /// Gets or sets the separator to display between the . + /// + public string Separator + { + get => (string)GetValue(SeparatorProperty); + set => SetValue(SeparatorProperty, value); + } + + /// + /// Gets or sets the separator that will be used to generate the accessible string representing the control content. + /// + public string AccessibleSeparator + { + get => (string)GetValue(AccessibleSeparatorProperty); + set => SetValue(AccessibleSeparatorProperty, value); + } + + /// + /// Gets or sets the to display in the control. + /// If it implements , the control will automatically update itself. + /// + public IEnumerable Items + { + get => (IEnumerable)GetValue(ItemsProperty); + set => SetValue(ItemsProperty, value); + } + + /// + /// Gets or sets the to use on the inner control. + /// + public Style TextBlockStyle + { + get => (Style)GetValue(TextBlockStyleProperty); + set => SetValue(TextBlockStyleProperty, value); + } + + /// + protected override void OnApplyTemplate() + { + _textContainer = GetTemplateChild(TextContainerPart) as TextBlock; + Update(); + } + + private static void OnMetadataItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + var control = (MetadataControl)d; + void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) => control.Update(); + + if (e.OldValue is INotifyCollectionChanged oldNcc) + { + oldNcc.CollectionChanged -= OnCollectionChanged; + } + + if (e.NewValue is INotifyCollectionChanged newNcc) + { + newNcc.CollectionChanged += OnCollectionChanged; + } + + control.Update(); + } + + private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + => ((MetadataControl)d).Update(); + + private void OnActualThemeChanged(FrameworkElement sender, object args) => Update(); + + private void Update() + { + if (_textContainer is null) + { + // The template is not ready yet. + return; + } + + _textContainer.Inlines.Clear(); + + if (Items is null) + { + AutomationProperties.SetName(_textContainer, string.Empty); + NotifyLiveRegionChanged(); + return; + } + + Inline unitToAppend; + var accessibleString = new StringBuilder(); + foreach (var unit in Items) + { + if (_textContainer.Inlines.Count > 0) + { + _textContainer.Inlines.Add(new Run { Text = Separator }); + accessibleString.Append(AccessibleSeparator ?? Separator); + } + + unitToAppend = new Run + { + Text = unit.Label, + }; + + if (unit.Command != null) + { + var hyperLink = new Hyperlink + { + UnderlineStyle = UnderlineStyle.None, + Foreground = _textContainer.Foreground, + }; + hyperLink.Inlines.Add(unitToAppend); + + void OnHyperlinkClicked(Hyperlink sender, HyperlinkClickEventArgs args) + { + if (unit.Command.CanExecute(unit.CommandParameter)) + { + unit.Command.Execute(unit.CommandParameter); + } + } + + hyperLink.Click += OnHyperlinkClicked; + + unitToAppend = hyperLink; + } + + var unitAccessibleLabel = unit.AccessibleLabel ?? unit.Label; + AutomationProperties.SetName(unitToAppend, unitAccessibleLabel); + accessibleString.Append(unitAccessibleLabel); + + _textContainer.Inlines.Add(unitToAppend); + } + + AutomationProperties.SetName(_textContainer, accessibleString.ToString()); + NotifyLiveRegionChanged(); + } + + private void NotifyLiveRegionChanged() + { + if (AutomationPeer.ListenerExists(AutomationEvents.LiveRegionChanged)) + { + var peer = FrameworkElementAutomationPeer.FromElement(this); + peer?.RaiseAutomationEvent(AutomationEvents.LiveRegionChanged); + } + } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataControl.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataControl.xaml new file mode 100644 index 00000000000..dea9a109cee --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataControl.xaml @@ -0,0 +1,15 @@ + + + + diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataItem.cs b/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataItem.cs new file mode 100644 index 00000000000..a8a5024167e --- /dev/null +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/MetadataControl/MetadataItem.cs @@ -0,0 +1,37 @@ +// 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 System.Windows.Input; + +namespace Microsoft.Toolkit.Uwp.UI.Controls +{ + /// + /// An item to display in . + /// + public struct MetadataItem + { + /// + /// Gets or sets the label of the item. + /// + public string Label { get; set; } + + /// + /// Gets or sets the automation name that will be set on the item. + /// If not set, will be used. + /// + public string AccessibleLabel { get; set; } + + /// + /// Gets or sets the command associated to the item. + /// If null, the item will be displayed as a text field. + /// If set, the item will be displayed as an hyperlink. + /// + public ICommand Command { get; set; } + + /// + /// Gets or sets the parameter that will be provided to the . + /// + public object CommandParameter { get; set; } + } +} diff --git a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml index 2959c0c3186..5a3aff27451 100644 --- a/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml +++ b/Microsoft.Toolkit.Uwp.UI.Controls.Core/Themes/Generic.xaml @@ -7,6 +7,7 @@ +