Silverlight中的AutoCompleteBox是一个非常强大的输入控件,可以实现灵活的参照录入。如果参照内容具有多个属性,则下拉列表使用多列的DataGrid是一个比较好的选择。
依靠控件模板强大的功能,AutoCompleteBox是可以实现这个需求的,并在Silverlight ToolKit Samples(November 2009)中给出了一个示例:AutoCompleteBox widht a DataGrid DropDown。示例中使用了一个从DataGrid继承的自定义的选择适配器(实现ISelectionAdapter接口),并最终用自定义AutoCompleteBox的控件模板实现。
从示例的实现过程可以看出,实现这个功能的工作量还是相当大的,如果项目中多处有多该功能的需求,则工作量几乎是不可接受的。于是想到了创建模板化控件。。。
1、首先,在项目中添加Silverlight模板化控件,取名为:AutoCompleteBoxPlus。
添加以后,VS自动为我们生成了一个从Control继承的类,还有一个Themes文件夹下的Generic.xaml。
2、修改AutoCompleteBoxPlus的基类,由Control改为AutoCompleteBox。
3、为AutoCompleteBoxPlus定义两个属性:PopupWidth(下拉列表宽度)及PopupColumns(下拉列表列集合)。本来是下拉的,不知自己为何用了Popup这个单词,呵呵。
至此,AutoCompleteBoxPlus类基本完成,完整代码如下:
/// <summary> /// 自动完成框扩展,支持下拉列表中使用DataGrid /// </summary> public class AutoCompleteBoxPlus : AutoCompleteBox { public AutoCompleteBoxPlus() { this.DefaultStyleKey = typeof(AutoCompleteBoxPlus); this.PopupWidth = this.Width; } private ObservableCollection<DataGridColumn> popupColumns = new ObservableCollection<DataGridColumn>(); public static readonly DependencyProperty PopupWidthProperty = DependencyProperty.Register("PopupWidth", typeof(double), typeof(AutoCompleteBoxPlus), null); /// <summary> /// 下拉列表宽度。 /// </summary> public double PopupWidth { get { return (double)GetValue(PopupWidthProperty); } set { SetValue(PopupWidthProperty, value); } } public override void OnApplyTemplate() { base.OnApplyTemplate(); DataGrid dg = this.SelectionAdapter as DataGrid; foreach (var column in this.PopupColumns) dg.Columns.Add(column); } /// <summary> /// 下拉列表列集合。 /// </summary> public ObservableCollection<DataGridColumn> PopupColumns { get { return this.popupColumns; } } }
4、在Generic.xaml中定义AutoCompleteBoxPlus控件的模板,模板大部分采用了Silverlight ToolKit Samples中的代码。
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:Demo"> <Style TargetType="local:AutoCompleteBoxPlus"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:AutoCompleteBoxPlus"> <Grid> <VisualStateManager.VisualStateGroups> <VisualStateGroup x:Name="PopupStates"> <VisualStateGroup.Transitions> <VisualTransition GeneratedDuration="0:0:0.2" To="PopupOpened" /> <VisualTransition GeneratedDuration="0:0:0.5" To="PopupClosed" /> </VisualStateGroup.Transitions> <VisualState x:Name="PopupOpened"> <Storyboard> <DoubleAnimation Storyboard.TargetName="PopupBorder" Storyboard.TargetProperty="Opacity" To="1.0" /> </Storyboard> </VisualState> <VisualState x:Name="PopupClosed"> <Storyboard> <DoubleAnimation Storyboard.TargetName="PopupBorder" Storyboard.TargetProperty="Opacity" To="0.0" /> </Storyboard> </VisualState> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <TextBox IsTabStop="True" x:Name="Text" Margin="0"/> <Popup x:Name="Popup"> <Border x:Name="PopupBorder" HorizontalAlignment="Stretch" Opacity="0.0" BorderThickness="0" CornerRadius="3"> <local:DataGridSelectionAdapter x:Name="SelectionAdapter" AutoGenerateColumns="False" IsReadOnly="True" HorizontalContentAlignment="Left" Width="{TemplateBinding PopupWidth}" > </local:DataGridSelectionAdapter> </Border> </Popup> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </ResourceDictionary>
需注意的是,模板中用到了一个自定义的类:DataGridSelectionAdapter,即上文介绍示例时提到的选择适配器了。该类直接从Silverlight ToolKit Samples中Reflector,然后略加修改。完整代码如下:
/// <summary> /// DataGrid选择适配器。 /// </summary> public class DataGridSelectionAdapter : DataGrid, ISelectionAdapter { public DataGridSelectionAdapter() { base.SelectionChanged += new SelectionChangedEventHandler(this.OnSelectionChanged); MouseLeftButtonUp += new MouseButtonEventHandler(this.OnSelectorMouseLeftButtonUp); } // Properties private bool IgnoreAnySelection { get; set; } private bool IgnoringSelectionChanged { get; set; } // Events public event RoutedEventHandler Cancel; public event RoutedEventHandler Commit; public new event SelectionChangedEventHandler SelectionChanged; private void AfterAdapterAction() { this.IgnoringSelectionChanged = true; this.SelectedItem = null; SelectedIndex = -1; this.IgnoringSelectionChanged = false; this.IgnoreAnySelection = true; } public AutomationPeer CreateAutomationPeer() { return new DataGridAutomationPeer(this); } public void HandleKeyDown(KeyEventArgs e) { Key key = e.Key; if (key != Key.Enter) { switch (key) { case Key.Up: this.IgnoreAnySelection = false; this.SelectedIndexDecrement(); e.Handled = true; return; case Key.Right: return; case Key.Down: if ((ModifierKeys.Alt & Keyboard.Modifiers) == ModifierKeys.None) { this.IgnoreAnySelection = false; this.SelectedIndexIncrement(); e.Handled = true; } return; case Key.Escape: this.OnCancel(this, e); e.Handled = true; return; } } else { this.OnCommit(this, e); e.Handled = true; } } private void OnCancel(object sender, RoutedEventArgs e) { RoutedEventHandler cancel = this.Cancel; if (cancel != null) { cancel(sender, e); } this.AfterAdapterAction(); } private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { this.IgnoreAnySelection = true; } private void OnCommit(object sender, RoutedEventArgs e) { RoutedEventHandler commit = this.Commit; if (commit != null) { commit(sender, e); } this.AfterAdapterAction(); } private void OnSelectionChanged(object sender, SelectionChangedEventArgs e) { if (!this.IgnoringSelectionChanged && !this.IgnoreAnySelection) { SelectionChangedEventHandler selectionChanged = this.SelectionChanged; if (selectionChanged != null) { selectionChanged(sender, e); } } } private void OnSelectorMouseLeftButtonUp(object sender, MouseButtonEventArgs e) { this.IgnoreAnySelection = false; this.OnSelectionChanged(this, null); this.OnCommit(this, new RoutedEventArgs()); } private void SelectedIndexDecrement() { int selectedIndex = SelectedIndex; if (selectedIndex >= 0) { SelectedIndex--; } else if (selectedIndex == -1) { SelectedIndex = this.Items.Count - 1; } ScrollIntoView(this.SelectedItem, Columns[0]); } private void SelectedIndexIncrement() { SelectedIndex = ((SelectedIndex + 1) >= this.Items.Count) ? -1 : (SelectedIndex + 1); ScrollIntoView(this.SelectedItem, Columns[0]); } private ObservableCollection<object> Items { get { return (this.ItemsSource as ObservableCollection<object>); } } public new IEnumerable ItemsSource { get { return base.ItemsSource; } set { INotifyCollectionChanged itemsSource; if (base.ItemsSource != null) { itemsSource = base.ItemsSource as INotifyCollectionChanged; if (itemsSource != null) { itemsSource.CollectionChanged -= new NotifyCollectionChangedEventHandler(this.OnCollectionChanged); } } base.ItemsSource = value; if (base.ItemsSource != null) { itemsSource = base.ItemsSource as INotifyCollectionChanged; if (itemsSource != null) { itemsSource.CollectionChanged += new NotifyCollectionChangedEventHandler(this.OnCollectionChanged); } } } } public new object SelectedItem { get { return base.SelectedItem; } set { this.IgnoringSelectionChanged = true; base.SelectedItem = value; this.IgnoringSelectionChanged = false; } } }
至此,一个支持DataGrid下拉列表的AutoCompleteBox控件就实现了。使用示例代码:
<local:AutoCompleteBoxPlus x:Name="acbp" ValueMemberPath="Name"> <local:AutoCompleteBoxPlus.PopupColumns> <data:DataGridTextColumn Header="Code" Binding="{Binding Code}" Width="80" /> <data:DataGridTextColumn Header="Name" Binding="{Binding Name}" Width="120" /> </local:AutoCompleteBoxPlus.PopupColumns> </local:AutoCompleteBoxPlus>
不过在实现过程中也着实遇到了一些问题,记录一下权作备忘:
A、自定义类中可以使用控件模板中的命名控件,但只能在重写的OnApplyTemplate方法中用GetTemplateChild方法获取,在不正确的时机或FindName方法是无法获取到的。
B、重写OnApplyTemplate方法时必须调用基类的该方法。
C、自定义的选择适配器必须命名为SelectionAdapter。全部AutoCompleteBox 控件的命名的部件可参见Silverlight文档中的“AutoCompleteBox 样式和模板”。