Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I create a tagging control similar to evernote in wpf?

Tags:

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

adding a tag

viewing existing tags and click 'x' to delete tag

showing existing tags and delete by clicking 'x'

like image 442
Kevin Avatar asked Mar 01 '13 22:03

Kevin


1 Answers

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.

enter image description here

enter image description here

enter image description here

enter image description here

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> 
like image 68
Mike Fuchs Avatar answered Oct 02 '22 20:10

Mike Fuchs