实现下拉列表支持DataGrid的AutoCompleteBox

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 样式和模板”。

你可能感兴趣的:(实现下拉列表支持DataGrid的AutoCompleteBox)