Skip to content

Instantly share code, notes, and snippets.

@gulbanana
Created August 25, 2016 07:18
Show Gist options
  • Save gulbanana/cc5f611c65c5f7e33123b2586b77ae35 to your computer and use it in GitHub Desktop.
Save gulbanana/cc5f611c65c5f7e33123b2586b77ae35 to your computer and use it in GitHub Desktop.
SuggestionBox control
public class SuggestionBox : Control
{
static SuggestionBox() { DefaultStyleKeyProperty.OverrideMetadata(typeof(SuggestionBox), new FrameworkPropertyMetadata(typeof(SuggestionBox))); }
public static DependencyProperty SelectedItemProperty =
DependencyProperty.Register("SelectedItem", typeof(string), typeof(SuggestionBox), new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (d, bv) => (d as SuggestionBox).OnSelectedItemChanged()));
public string SelectedItem
{
get { return (string)GetValue(SelectedItemProperty); }
set { SetValue(SelectedItemProperty, value); }
}
public static DependencyProperty ItemsSourceProperty =
DependencyProperty.Register("ItemsSource", typeof(IEnumerable<string>), typeof(SuggestionBox));
public IEnumerable<string> ItemsSource
{
get { return (IEnumerable<string>)GetValue(ItemsSourceProperty); }
set { SetValue(ItemsSourceProperty, value); }
}
public static DependencyProperty SelectedBackgroundBrushProperty =
DependencyProperty.Register("SelectedBackgroundBrush", typeof(Brush), typeof(SuggestionBox));
public Brush SelectedBackgroundBrush
{
get { return (Brush)GetValue(SelectedBackgroundBrushProperty); }
set { SetValue(SelectedBackgroundBrushProperty, value); }
}
public static DependencyProperty SelectOnFocusLossProperty =
DependencyProperty.Register("SelectOnFocusLoss", typeof(bool), typeof(SuggestionBox), new FrameworkPropertyMetadata(false));
public bool SelectOnFocusLoss
{
get { return (bool)GetValue(SelectOnFocusLossProperty); }
set { SetValue(SelectOnFocusLossProperty, value); }
}
private TextBox PART_TextBox;
private ItemsControl PART_ItemsControl;
private Popup PART_Popup;
public IReadOnlyList<string> CurrentSuggestions { get; private set; }
public IReadOnlyList<string> FilteredSuggestions { get; private set; }
public int SelectedIndex { get; private set; }
public SuggestionBox()
{
CurrentSuggestions = new string[] { };
SelectedIndex = 0;
}
private void UpdateSuggestions()
{
if (CurrentSuggestions.Any(s => object.ReferenceEquals(s, SelectedItem)))
{
if (PART_Popup != null) PART_Popup.IsOpen = false;
}
else
{
if (PART_Popup != null) PART_Popup.IsOpen = true;
FilteredSuggestions = CurrentSuggestions.Where(s => s.ToLower().Contains(SelectedItem.ToLower())).ToList();
SelectedIndex = Math.Min(SelectedIndex, FilteredSuggestions.Count() - 1);
if (PART_ItemsControl != null) PART_ItemsControl.ItemsSource = FilteredSuggestions.Select((s, i) => new SuggestionBoxItem
{
Text = s,
Background = (i == SelectedIndex) ? SelectedBackgroundBrush : null,
OnMouseDown = new DelegateCommand<SuggestionBoxItem>(OnItemClicked)
});
}
}
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
PART_TextBox = Template.FindName(nameof(PART_TextBox), this) as TextBox;
PART_TextBox.TextChanged += OnTextChanged;
PART_ItemsControl = Template.FindName(nameof(PART_ItemsControl), this) as ItemsControl;
PART_Popup = Template.FindName(nameof(PART_Popup), this) as Popup;
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
SelectedItem = PART_TextBox.Text;
}
private void OnItemClicked(SuggestionBoxItem s)
{
SelectedItem = s.Text;
}
private void OnSelectedItemChanged()
{
UpdateSuggestions();
}
protected override void OnGotKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
CurrentSuggestions = ItemsSource.ToList();
SelectedIndex = 0;
UpdateSuggestions();
}
public void SetGotFocus() => OnGotKeyboardFocus(null);
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
if (PART_Popup != null) PART_Popup.IsOpen = false;
if (SelectOnFocusLoss)
{
SelectedItem = FilteredSuggestions.ElementAtOrDefault(SelectedIndex) ?? SelectedItem;
}
}
public void SetLostFocus() => OnLostKeyboardFocus(null);
protected override void OnPreviewKeyDown(KeyEventArgs e)
{
base.OnPreviewKeyDown(e);
if (e.Key == Key.Down && SelectedIndex < CurrentSuggestions.Count - 1)
{
SelectedIndex++;
UpdateSuggestions();
}
else if (e.Key == Key.Up && SelectedIndex > 0)
{
SelectedIndex--;
UpdateSuggestions();
}
else if (e.Key == Key.Enter)
{
SelectedItem = FilteredSuggestions.ElementAtOrDefault(SelectedIndex) ?? SelectedItem;
}
}
}
<ResourceDictionary x:Class="The.WPF.Controls.SuggestionBoxStyle" x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:The.WPF.Controls">
<Style TargetType="{x:Type controls:SuggestionBox}">
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="SelectedBackgroundBrush" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}"/>
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}"/>
<Setter Property="BorderThickness" Value="1,0,1,1"/>
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type controls:SuggestionBox}">
<StackPanel Orientation="Vertical">
<TextBox x:Name="PART_TextBox" Text="{TemplateBinding SelectedItem}"/>
<Popup x:Name="PART_Popup" IsOpen="False" Visibility="Collapsed" AllowsTransparency="True" Width="{Binding ActualWidth,ElementName=PART_TextBox}">
<ItemsControl x:Name="PART_ItemsControl" IsTabStop="False" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid MouseDown="Item_MouseDown" Tag="{Binding OnMouseDown}" Height="22">
<Border Background="{Binding Background}" Opacity="0.25"/>
<TextBlock Text="{Binding Text}" VerticalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Popup>
</StackPanel>
<ControlTemplate.Triggers>
<Trigger Property="IsKeyboardFocusWithin" Value="False">
<Setter TargetName="PART_ItemsControl" Property="BorderThickness" Value="0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment