I like the tagging control in Evernote (windows version) and was wondering if there is something similar out there? I have only been able to find tag cloud controls.
Specifically, I like the free format typing like in a text box that looks up and presents Intellisense style the tags that match what I have typed. When I select a tag, the text is replaced with a button representing the tag with the text of the button being the tag text.
Update: adding screenshots
Adding a new tag
viewing existing tags and click 'x' to delete tag
This seemed like a really nice exercise, so I tried to build this control. I didn't test it thoroughly, let me know if you want to work with it and need further help.
Example usage:
<Window x:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1" xmlns:s="clr-namespace:System;assembly=mscorlib" Title="MainWindow" Height="350" Width="525"> <Window.DataContext> <local:ViewModel /> </Window.DataContext> <Grid> <!-- todo: implement ICommand properties on EvernoteTagControl to allow easy binding to the viewmodel. Alternatively, the user could use a behavior to handle TagClick, and if necessary TagAdded/TagRemoved --> <local:EvernoteTagControl ItemsSource="{Binding SelectedTags}" TagClick="TagControl_TagClick" > <local:EvernoteTagControl.AllTags> <s:String>read</s:String> <s:String>receipt</s:String> <s:String>recipe</s:String> <s:String>research</s:String> <s:String>restaurants</s:String> </local:EvernoteTagControl.AllTags> </local:EvernoteTagControl> </Grid> </Window>
ViewModel:
using System.Collections.Generic; using System.ComponentModel; namespace WpfApplication1 { public class ViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private List<EvernoteTagItem> _selectedTags = new List<EvernoteTagItem>(); public List<EvernoteTagItem> SelectedTags { get { return _selectedTags; } set { _selectedTags = value; if (_selectedTags != value) OnPropertyChanged("SelectedTags"); } } public ViewModel() { this.SelectedTags = new List<EvernoteTagItem>() { new EvernoteTagItem("news"), new EvernoteTagItem("priority") }; } private void OnPropertyChanged(string propertyName) { if (this.PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } }
EvernoteTagControl:
using System; using System.Collections; using System.Collections.Generic; using System.Windows; using System.Windows.Controls; namespace WpfApplication1 { [TemplatePart(Name = "PART_CreateTagButton", Type = typeof(Button))] public class EvernoteTagControl : ListBox { public event EventHandler<EvernoteTagEventArgs> TagClick; public event EventHandler<EvernoteTagEventArgs> TagAdded; public event EventHandler<EvernoteTagEventArgs> TagRemoved; static EvernoteTagControl() { // lookless control, get default style from generic.xaml DefaultStyleKeyProperty.OverrideMetadata(typeof(EvernoteTagControl), new FrameworkPropertyMetadata(typeof(EvernoteTagControl))); } public EvernoteTagControl() { //// some dummy data, this needs to be provided by user //this.ItemsSource = new List<EvernoteTagItem>() { new EvernoteTagItem("receipt"), new EvernoteTagItem("restaurant") }; //this.AllTags = new List<string>() { "recipe", "red" }; } // AllTags public List<string> AllTags { get { return (List<string>)GetValue(AllTagsProperty); } set { SetValue(AllTagsProperty, value); } } public static readonly DependencyProperty AllTagsProperty = DependencyProperty.Register("AllTags", typeof(List<string>), typeof(EvernoteTagControl), new PropertyMetadata(new List<string>())); // IsEditing, readonly public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } internal set { SetValue(IsEditingPropertyKey, value); } } private static readonly DependencyPropertyKey IsEditingPropertyKey = DependencyProperty.RegisterReadOnly("IsEditing", typeof(bool), typeof(EvernoteTagControl), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty IsEditingProperty = IsEditingPropertyKey.DependencyProperty; public override void OnApplyTemplate() { Button createBtn = this.GetTemplateChild("PART_CreateTagButton") as Button; if (createBtn != null) createBtn.Click += createBtn_Click; base.OnApplyTemplate(); } /// <summary> /// Executed when create new tag button is clicked. /// Adds an EvernoteTagItem to the collection and puts it in edit mode. /// </summary> void createBtn_Click(object sender, RoutedEventArgs e) { var newItem = new EvernoteTagItem() { IsEditing = true }; AddTag(newItem); this.SelectedItem = newItem; this.IsEditing = true; } /// <summary> /// Adds a tag to the collection /// </summary> internal void AddTag(EvernoteTagItem tag) { if (this.ItemsSource == null) this.ItemsSource = new List<EvernoteTagItem>(); ((IList)this.ItemsSource).Add(tag); // assume IList for convenience this.Items.Refresh(); if (TagAdded != null) TagAdded(this, new EvernoteTagEventArgs(tag)); } /// <summary> /// Removes a tag from the collection /// </summary> internal void RemoveTag(EvernoteTagItem tag, bool cancelEvent = false) { if (this.ItemsSource != null) { ((IList)this.ItemsSource).Remove(tag); // assume IList for convenience this.Items.Refresh(); if (TagRemoved != null && !cancelEvent) TagRemoved(this, new EvernoteTagEventArgs(tag)); } } /// <summary> /// Raises the TagClick event /// </summary> internal void RaiseTagClick(EvernoteTagItem tag) { if (this.TagClick != null) TagClick(this, new EvernoteTagEventArgs(tag)); } } public class EvernoteTagEventArgs : EventArgs { public EvernoteTagItem Item { get; set; } public EvernoteTagEventArgs(EvernoteTagItem item) { this.Item = item; } } }
EvernoteTagItem:
using System.Collections; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; namespace WpfApplication1 { [TemplatePart(Name = "PART_InputBox", Type = typeof(AutoCompleteBox))] [TemplatePart(Name = "PART_DeleteTagButton", Type = typeof(Button))] [TemplatePart(Name = "PART_TagButton", Type = typeof(Button))] public class EvernoteTagItem : Control { static EvernoteTagItem() { // lookless control, get default style from generic.xaml DefaultStyleKeyProperty.OverrideMetadata(typeof(EvernoteTagItem), new FrameworkPropertyMetadata(typeof(EvernoteTagItem))); } public EvernoteTagItem() { } public EvernoteTagItem(string text) : this() { this.Text = text; } // Text public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } } public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(EvernoteTagItem), new PropertyMetadata(null)); // IsEditing, readonly public bool IsEditing { get { return (bool)GetValue(IsEditingProperty); } internal set { SetValue(IsEditingPropertyKey, value); } } private static readonly DependencyPropertyKey IsEditingPropertyKey = DependencyProperty.RegisterReadOnly("IsEditing", typeof(bool), typeof(EvernoteTagItem), new FrameworkPropertyMetadata(false)); public static readonly DependencyProperty IsEditingProperty = IsEditingPropertyKey.DependencyProperty; /// <summary> /// Wires up delete button click and focus lost /// </summary> public override void OnApplyTemplate() { AutoCompleteBox inputBox = this.GetTemplateChild("PART_InputBox") as AutoCompleteBox; if (inputBox != null) { inputBox.LostFocus += inputBox_LostFocus; inputBox.Loaded += inputBox_Loaded; } Button btn = this.GetTemplateChild("PART_TagButton") as Button; if (btn != null) { btn.Loaded += (s, e) => { Button b = s as Button; var btnDelete = b.Template.FindName("PART_DeleteTagButton", b) as Button; // will only be found once button is loaded if (btnDelete != null) { btnDelete.Click -= btnDelete_Click; // make sure the handler is applied just once btnDelete.Click += btnDelete_Click; } }; btn.Click += (s, e) => { var parent = GetParent(); if (parent != null) parent.RaiseTagClick(this); // raise the TagClick event of the EvernoteTagControl }; } base.OnApplyTemplate(); } /// <summary> /// Handles the click on the delete glyph of the tag button. /// Removes the tag from the collection. /// </summary> void btnDelete_Click(object sender, RoutedEventArgs e) { var item = FindUpVisualTree<EvernoteTagItem>(sender as FrameworkElement); var parent = GetParent(); if (item != null && parent != null) parent.RemoveTag(item); e.Handled = true; // bubbling would raise the tag click event } /// <summary> /// When an AutoCompleteBox is created, set the focus to the textbox. /// Wire PreviewKeyDown event to handle Escape/Enter keys /// </summary> /// <remarks>AutoCompleteBox.Focus() is broken: http://stackoverflow.com/questions/3572299/autocompletebox-focus-in-wpf</remarks> void inputBox_Loaded(object sender, RoutedEventArgs e) { AutoCompleteBox acb = sender as AutoCompleteBox; if (acb != null) { var tb = acb.Template.FindName("Text", acb) as TextBox; if (tb != null) tb.Focus(); // PreviewKeyDown, because KeyDown does not bubble up for Enter acb.PreviewKeyDown += (s, e1) => { var parent = GetParent(); if (parent != null) { switch (e1.Key) { case (Key.Enter): // accept tag parent.Focus(); break; case (Key.Escape): // reject tag parent.Focus(); parent.RemoveTag(this, true); // do not raise RemoveTag event break; } } }; } } /// <summary> /// Set IsEditing to false when the AutoCompleteBox loses keyboard focus. /// This will change the template, displaying the tag as a button. /// </summary> void inputBox_LostFocus(object sender, RoutedEventArgs e) { this.IsEditing = false; var parent = GetParent(); if (parent != null) parent.IsEditing = false; } private EvernoteTagControl GetParent() { return FindUpVisualTree<EvernoteTagControl>(this); } /// <summary> /// Walks up the visual tree to find object of type T, starting from initial object /// http://www.codeproject.com/Tips/75816/Walk-up-the-Visual-Tree /// </summary> private static T FindUpVisualTree<T>(DependencyObject initial) where T : DependencyObject { DependencyObject current = initial; while (current != null && current.GetType() != typeof(T)) { current = VisualTreeHelper.GetParent(current); } return current as T; } } }
Themes/generic.xaml:
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:WpfApplication1" xmlns:tkInput="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"> <SolidColorBrush x:Key="HighlightBrush" Color="DodgerBlue" /> <!-- EvernoteTagControl default style --> <Style TargetType="{x:Type local:EvernoteTagControl}"> <Style.Resources> <SolidColorBrush x:Key="{x:Static SystemColors.HighlightBrushKey}" Color="White"/> <SolidColorBrush x:Key="{x:Static SystemColors.ControlBrushKey}" Color="White" /> <LinearGradientBrush x:Key="IconBrush" EndPoint="0,1"> <GradientStop Color="#5890f0" Offset="0" /> <GradientStop Color="#0351d7" Offset="1" /> </LinearGradientBrush> </Style.Resources> <Setter Property="FocusVisualStyle" Value="{x:Null}" /> <Setter Property="VerticalAlignment" Value="Top" /> <Setter Property="Margin" Value="5" /> <Setter Property="MinHeight" Value="25" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="SnapsToDevicePixels" Value="True" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:EvernoteTagControl}"> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <Path Grid.Column="0" Margin="2" Fill="{StaticResource IconBrush}" Height="19" Stretch="Uniform" Data="M 50.535714,0.44196425 0.00446427,34.754464 l 0,106.906246 100.71874573,0 0,-107.124996 L 50.535714,0.44196425 z m 0.1875,21.21874975 c 6.311826,0 11.40625,5.094424 11.40625,11.40625 0,6.311826 -5.094424,11.4375 -11.40625,11.4375 -6.311826,0 -11.4375,-5.125674 -11.4375,-11.4375 0,-6.311826 5.125674,-11.40625 11.4375,-11.40625 z" /> <ItemsPresenter Grid.Column="1" /> <Button Margin="5,0,0,0" Grid.Column="2" Content="Click to add tag..." x:Name="PART_CreateTagButton"> <Button.Template> <ControlTemplate TargetType="Button"> <ContentPresenter TextElement.Foreground="#FF555555" VerticalAlignment="Center" /> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Cursor" Value="Hand" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Button.Template> </Button> </Grid> <ControlTemplate.Triggers> <Trigger Property="IsEditing" Value="True"> <Setter TargetName="PART_CreateTagButton" Property="Visibility" Value="Collapsed" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Setter.Value> </Setter> <Setter Property="ItemContainerStyle"> <Setter.Value> <Style TargetType="{x:Type ListBoxItem}"> <Setter Property="FocusVisualStyle" Value="{x:Null}" /> </Style> </Setter.Value> </Setter> <Setter Property="ItemsPanel" > <Setter.Value> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" /> </ItemsPanelTemplate> </Setter.Value> </Setter> </Style> <!-- EvernoteTagItem default style --> <Style TargetType="{x:Type local:EvernoteTagItem}"> <Setter Property="FocusVisualStyle" Value="{x:Null}" /> <Setter Property="MinWidth" Value="50" /> <Setter Property="Margin" Value="0,0,2,0" /> <Setter Property="Padding" Value="5,2,0,2" /> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:EvernoteTagItem}"> <Button x:Name="PART_TagButton" Content="{TemplateBinding Text}" Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}"> <Button.Template> <ControlTemplate TargetType="Button"> <Border Margin="{TemplateBinding Margin}" Padding="{TemplateBinding Padding}" BorderBrush="Gray" BorderThickness="1" CornerRadius="2" Background="#01FFFFFF"> <Grid > <Grid.ColumnDefinitions> <ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" /> </Grid.ColumnDefinitions> <ContentPresenter VerticalAlignment="Center" HorizontalAlignment="Left" Margin="0,0,0,2" /> <Button x:Name="PART_DeleteTagButton" Grid.Column="1" Margin="3,0" VerticalAlignment="Center" HorizontalAlignment="Right" Visibility="Hidden" > <Button.Template> <ControlTemplate> <Grid Height="10" Width="10" Background="#01FFFFFF" > <Path Stretch="Uniform" ClipToBounds="True" Stroke="{StaticResource HighlightBrush}" StrokeThickness="2" Data="M 85.364473,6.9977109 6.0640998,86.29808 6.5333398,85.76586 M 6.9926698,7.4977169 86.293043,86.79809 85.760823,86.32885" /> </Grid> </ControlTemplate> </Button.Template> </Button> </Grid> </Border> <ControlTemplate.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="Foreground" Value="{StaticResource HighlightBrush}" /> <Setter TargetName="PART_DeleteTagButton" Property="Visibility" Value="Visible" /> </Trigger> </ControlTemplate.Triggers> </ControlTemplate> </Button.Template> </Button> </ControlTemplate> </Setter.Value> </Setter> <Style.Triggers> <Trigger Property="IsEditing" Value="True"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type local:EvernoteTagItem}"> <tkInput:AutoCompleteBox x:Name="PART_InputBox" Text="{Binding Text, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" ItemsSource="{Binding AllTags, RelativeSource={RelativeSource AncestorType={x:Type local:EvernoteTagControl}}}" IsTextCompletionEnabled="True" /> </ControlTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> </ResourceDictionary>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With