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.
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:
read
receipt
recipe
research
restaurants
ViewModel:
using System.Collections.Generic;
using System.ComponentModel;
namespace WpfApplication1
{
public class ViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private List _selectedTags = new List();
public List SelectedTags
{
get { return _selectedTags; }
set
{
_selectedTags = value;
if (_selectedTags != value)
OnPropertyChanged("SelectedTags");
}
}
public ViewModel()
{
this.SelectedTags = new List() { 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 TagClick;
public event EventHandler TagAdded;
public event EventHandler 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() { new EvernoteTagItem("receipt"), new EvernoteTagItem("restaurant") };
//this.AllTags = new List() { "recipe", "red" };
}
// AllTags
public List AllTags { get { return (List)GetValue(AllTagsProperty); } set { SetValue(AllTagsProperty, value); } }
public static readonly DependencyProperty AllTagsProperty = DependencyProperty.Register("AllTags", typeof(List), typeof(EvernoteTagControl), new PropertyMetadata(new List()));
// 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();
}
///
/// Executed when create new tag button is clicked.
/// Adds an EvernoteTagItem to the collection and puts it in edit mode.
///
void createBtn_Click(object sender, RoutedEventArgs e)
{
var newItem = new EvernoteTagItem() { IsEditing = true };
AddTag(newItem);
this.SelectedItem = newItem;
this.IsEditing = true;
}
///
/// Adds a tag to the collection
///
internal void AddTag(EvernoteTagItem tag)
{
if (this.ItemsSource == null)
this.ItemsSource = new List();
((IList)this.ItemsSource).Add(tag); // assume IList for convenience
this.Items.Refresh();
if (TagAdded != null)
TagAdded(this, new EvernoteTagEventArgs(tag));
}
///
/// Removes a tag from the collection
///
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));
}
}
///
/// Raises the TagClick event
///
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;
///
/// Wires up delete button click and focus lost
///
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();
}
///
/// Handles the click on the delete glyph of the tag button.
/// Removes the tag from the collection.
///
void btnDelete_Click(object sender, RoutedEventArgs e)
{
var item = FindUpVisualTree(sender as FrameworkElement);
var parent = GetParent();
if (item != null && parent != null)
parent.RemoveTag(item);
e.Handled = true; // bubbling would raise the tag click event
}
///
/// When an AutoCompleteBox is created, set the focus to the textbox.
/// Wire PreviewKeyDown event to handle Escape/Enter keys
///
/// AutoCompleteBox.Focus() is broken: http://stackoverflow.com/questions/3572299/autocompletebox-focus-in-wpf
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;
}
}
};
}
}
///
/// Set IsEditing to false when the AutoCompleteBox loses keyboard focus.
/// This will change the template, displaying the tag as a button.
///
void inputBox_LostFocus(object sender, RoutedEventArgs e)
{
this.IsEditing = false;
var parent = GetParent();
if (parent != null)
parent.IsEditing = false;
}
private EvernoteTagControl GetParent()
{
return FindUpVisualTree(this);
}
///
/// 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
///
private static T FindUpVisualTree(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: