【问题标题】:WPF AutoCompleteBox dynamic sortingWPF AutoCompleteBox 动态排序
【发布时间】:2013-03-23 04:50:08
【问题描述】:

我正在使用 WPF AutoCompleteBox,它运行良好,但我想做的一件事是在将每个字母输入到主 TextBox 后即时对建议列表进行排序。有谁知道如何做到这一点?我尝试将 ICollectionView 属性与 DefaultView 逻辑一起使用并添加 SortDescriptions,但它似乎没有分阶段建议列表。为了确保我的集合视图排序正常工作,我将一个普通的 ListBox 控件和一个 AutoCompleteBox 控件放在同一个窗口上,并将两个控件绑定到具有相同集合视图的同一个可观察集合,并且普通的 ListBox 控件显示使用 SortDescriptions 正确排序的项目,但 AutoCompleteBox 列表没有对项目进行排序。它按照它们添加到集合中的顺序排列它们。

想法?建议?有人做过吗?

【问题讨论】:

  • 愿意分享您的解决方案吗?

标签: wpf autocomplete wpf-controls observablecollection wpftoolkit


【解决方案1】:

我不知道@user1089031 是如何做到这一点的,但这里是任何可能感兴趣的人的工作示例(更新为@adabyron 的评论!):

ViewModel.cs

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Windows.Data;

namespace WpfApplication12
{
    public class Item
    {
        public string Name { get; set; }

        public override string ToString()
        {
            return Name;
        }
    }

    public class ViewModel: INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged = delegate {};

        private readonly ObservableCollection<Item> source;
        private readonly ICollectionView items;
        private string searchText;

        public ViewModel()
        {
            source = new ObservableCollection<Item>
                         {
                             new Item {Name = "111111111 Test abb - (1)"},
                             new Item {Name = "22222 Test - (2)"},
                             new Item {Name = "333 Test - (3)"},
                             new Item {Name = "44444 Test abc - (4)"},
                             new Item {Name = "555555 Test cde - (5)"},
                             new Item {Name = "66 Test - bbcd (6)"},
                             new Item {Name = "7 Test - cd (7)"},
                             new Item {Name = "Test - ab (8)"},
                         };

            items = new ListCollectionView(source);
        }

        public ICollectionView Items
        {
            get { return items; }
        }

        public IEnumerable<Item> ItemsSorted
        {
            get 
            {
                return string.IsNullOrEmpty(SearchText)
                        ? source
                        : (IEnumerable<Item>)source
                            .OrderBy(item => item.Name.IndexOf(SearchText,
                                StringComparison.InvariantCultureIgnoreCase));
            }
        }

        public Item Selected { get; set; }

        public string SearchText
        {
            get { return searchText; }
            set
            {
                searchText = value;
                PropertyChanged(this,
                            new PropertyChangedEventArgs("SearchText"));
                PropertyChanged(this,
                            new PropertyChangedEventArgs("ItemsSorted"));
            }
        }
    }
}

MainWindow.xaml

<Window x:Class="WpfApplication12.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:controls="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Input.Toolkit"
        xmlns:wpfApplication2="clr-namespace:WpfApplication12"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        Title="MainWindow" Height="200" Width="500"
        DataContext="{DynamicResource viewModel}">

    <Window.Resources>

        <wpfApplication2:ViewModel x:Key="viewModel" />

        <DataTemplate DataType="{x:Type wpfApplication2:Item}">
            <TextBlock Text="{Binding Name}" FontFamily="Courier New" />
        </DataTemplate>

    </Window.Resources>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition />
            <ColumnDefinition />
        </Grid.ColumnDefinitions>

        <controls:AutoCompleteBox
            ItemsSource="{Binding ItemsSorted}"
            FilterMode="ContainsOrdinal"
            SelectedItem="{Binding Selected, Mode=TwoWay}"
            MinimumPrefixLength="0" 
            VerticalAlignment="Top" Margin="5">

            <i:Interaction.Behaviors>
                <wpfApplication2:SearchTextBindBehavior
                            BoundSearchText="{Binding SearchText,
                                                      Mode=OneWayToSource}" />
            </i:Interaction.Behaviors>

        </controls:AutoCompleteBox>

        <ListBox Grid.Column="1"
                 ItemsSource="{Binding Items}" Margin="5" />

    </Grid>
</Window>

如您所见,我向AutoCompleteBox 控件添加了一种自定义行为:

<i:Interaction.Behaviors>
    <wpfApplication2:SearchTextBindBehavior
            BoundSearchText="{Binding SearchText,
                                      Mode=OneWayToSource}" />
</i:Interaction.Behaviors>

这是因为AutoCompleteBox 自己的SearchText 属性是只读的。所以这里是这个行为的代码:

SearchTextBindBehavior.cs更新

using System.Windows;
using System.Windows.Controls;
using System.Windows.Interactivity;

namespace WpfApplication12
{
    public class SearchTextBindBehavior : Behavior<AutoCompleteBox>
    {
        public static readonly DependencyProperty BoundSearchTextProperty =
                            DependencyProperty.Register("BoundSearchText",
                            typeof(string), typeof(SearchTextBindBehavior));

        public string BoundSearchText
        {
            get { return (string)GetValue(BoundSearchTextProperty); }
            set { SetValue(BoundSearchTextProperty, value); }
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.TextChanged += OnTextChanged;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.TextChanged -= OnTextChanged;
        }

        private void OnTextChanged(object sender, RoutedEventArgs args)
        {
            if(AssociatedObject.Text.Length == 0)
            {
                BoundSearchText = string.Empty;
                return;
            }

            if(AssociatedObject.SearchText ==
                AssociatedObject.Text.Substring(0,
                                           AssociatedObject.Text.Length - 1))
            {
                BoundSearchText = AssociatedObject.Text;
            }
        }
    }
}

注意:要使这一切正常工作,您需要添加对来自Expression Blend 4 SDKSystem.Windows.Interactivity.dll 的引用。这正是Behavior&lt;T&gt; 和它的几个朋友居住的地方。

如果您已经安装了 Expression Blend,那么您已经拥有了所有 SDK,无需下载任何东西。以防万一 - 在我的机器上,组件位于此处:

C:\Program Files\Microsoft SDKs\Expression\Blend.NETFramework\v4.0\Libraries\System.Windows.Interactivity.dll

最后,如果您有充分的理由不添加对这个流行的官方库的引用,请随时通过普通的旧附加属性以“旧方式”重新实现此自定义行为。

希望对您有所帮助。

【讨论】:

  • 虽然这是一个很好的例子,但它并没有真正回答问题,因为不涉及 动态 排序。我希望根据搜索文本在其中出现的时间对项目进行排序。
  • 哦,这很有趣,@adabyron。如果我成功了,我会更新答案。
  • 感谢您的努力,Sevenate!这在一定程度上解决了这个问题。但是,有了它,不幸的是,我不能再通过上/下键浏览建议。附带说明:可以绑定到 Text 属性而不是 SearchText,从而避免该行为。然而,这本身不是问题,我对行为很好。
  • @adabyron 我注意到上/下键的这个问题,也许我也可以解决这个问题(会尝试)。至于“而不是 SearchText” - 如果你在谈论这个 - BoundSearchText="{Binding SearchText, ...这只是 ViewModel 类的属性而不是 AutoCompleteBox 元素。不好 - 我应该选择另一个名称来防止任何混乱。
  • 我的意思是,AutoCompleteBox上有一个Text属性,可以绑定TwoWay..
【解决方案2】:

这就是我最终得到的结果,对 Sevenate 的回答稍作改编,所以如果你想投票,请在他的帖子中投赞成票。

我使用了一个子类(由于其他原因,我已经将 AutoCompleteBox 子类化了),它允许我创建一个包装器依赖属性以将只读的 SearchText(=用户通过键盘输入的内容)获取到 ViewModel -而不是混合行为,这也是一种完全有效的方式。

问题的症结在于,您应该只对 SearchText 的更改应用动态排序,而不是 Text(=AutoCompleteBox 中显示的内容,如果在下拉列表中选择建议,也会更改)。 Sevenate 引发只读 ItemsSource (ItemsSorted) 的 PropertyChanged 事件的方法是应用排序的好方法。

视图模型:

public class Item
{
    public string Name { get; set; }

    public override string ToString()
    {
        return Name;
    }
}

public class AutoCompleteBoxDynamicSortingVM : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private readonly ObservableCollection<Item> source;

    public AutoCompleteBoxDynamicSortingVM()
    {
        source = new ObservableCollection<Item>
                     {
                         new Item {Name = "111111111 Test abb - (1)"},
                         new Item {Name = "22222 Test - (2)"},
                         new Item {Name = "333 Test - (3)"},
                         new Item {Name = "44444 Test abc - (4)"},
                         new Item {Name = "555555 Test cde - (5)"},
                         new Item {Name = "66 Test - bbcd (6)"},
                         new Item {Name = "7 Test - cd (7)"},
                         new Item {Name = "Test - ab (8)"},
                     };
    }

    public IEnumerable<Item> ItemsSorted
    {
        get
        {
            return string.IsNullOrEmpty(Text) ? (IEnumerable<Item>)source :
                    source.OrderBy(item => item.Name.IndexOf(Text, StringComparison.OrdinalIgnoreCase));
        }
    }

    public Item Selected { get; set; }

    // Text that is shown in AutoCompleteBox
    private string text;
    public string Text
    {
        get { return text; }
        set { text = value; OnPropertyChanged("Text"); }
    }

    // Text that was entered by user (cannot be changed from viewmodel)
    private string searchText;
    public string SearchText
    {
        get { return searchText; }
        set
        {
            searchText = value;
            OnPropertyChanged("SearchText");
            OnPropertyChanged("ItemsSorted");
        }
    }

    private void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

AutoCompleteBox 的子类:

public class MyAutoCompleteBox : AutoCompleteBox
{
    /// <summary>
    /// Bindable property that encapsulates the readonly property SearchText.
    /// When the viewmodel tries to set SearchText by way of EnteredText, it will fail without an exception.
    /// </summary>
    public string EnteredText
    {
        get { return (string)GetValue(EnteredTextProperty); }
        set { SetValue(EnteredTextProperty, value); } 
    }
    public static readonly DependencyProperty EnteredTextProperty = DependencyProperty.Register("EnteredText", typeof(string), typeof(MyAutoCompleteBox), new PropertyMetadata(null));


    protected override void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
    {
        // synchronize SearchText and EnteredText (only one-way)
        if (e.Property == AutoCompleteBox.SearchTextProperty && this.EnteredText != this.SearchText)
            EnteredText = SearchText;

        base.OnPropertyChanged(e);
    }
}

Xaml:

<UserControl x:Class="WpfApplication1.Controls.AutoCompleteBoxDynamicSorting"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:myctrls="clr-namespace:WpfApplication1.Controls"
        xmlns:models="clr-namespace:WpfApplication1.ViewModels"
        Height="350" Width="525"
        DataContext="{DynamicResource viewModel}">

    <UserControl.Resources>

        <models:AutoCompleteBoxDynamicSortingVM x:Key="viewModel" />

        <DataTemplate DataType="{x:Type models:Item}">
            <TextBlock Text="{Binding Path=Name}" />
        </DataTemplate>

    </UserControl.Resources>

    <Grid>
        <myctrls:MyAutoCompleteBox
            ItemsSource="{Binding ItemsSorted}"
            Text="{Binding Text, Mode=TwoWay}"
            EnteredText="{Binding SearchText, Mode=OneWayToSource}"
            FilterMode="ContainsOrdinal"
            VerticalAlignment="Top" Margin="5" />
    </Grid>
</UserControl>

【讨论】:

  • 实际上,我应该承认,尽管我对从 UI 元素进行子类化并不是很有趣 - 如果我们需要一个干净简单的方法,看起来你的解决方案是唯一的方法在复制/粘贴到 AutoCompleteBox 字段期间没有故障的代码。因此,如果您的解决方案被标记为该问题的答案,那将是公平的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-06-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多