web-dev-qa-db-ja.com

CollectionViewSourceのトリガーフィルター

MVVMパターンを使用して、WPFデスクトップアプリケーションで作業しています。

ListViewに入力されたテキストに基づいて、TextBoxからいくつかのアイテムを除外しようとしています。テキストを変更するときにListViewアイテムをフィルター処理したい。

フィルターテキストが変更されたときにフィルターをトリガーする方法を知りたい

ListViewは、CollectionViewSourceにバインドします。これは、ViewModelのObservableCollectionにバインドします。フィルターテキストのTextBoxは、必要に応じて_UpdateSourceTrigger=PropertyChanged_を使用して、ViewModelの文字列にバインドします。

_<CollectionViewSource x:Key="ProjectsCollection"
                      Source="{Binding Path=AllProjects}"
                      Filter="CollectionViewSource_Filter" />

<TextBox Text="{Binding Path=FilterText, UpdateSourceTrigger=PropertyChanged}" />

<ListView DataContext="{StaticResource ProjectsCollection}"
          ItemsSource="{Binding}" />
_

_Filter="CollectionViewSource_Filter"_は、コードビハインドのイベントハンドラーにリンクします。これは、ViewModelでフィルターメソッドを呼び出すだけです。

FilterTextの値が変更されると、フィルタリングが実行されます。FilterTextプロパティのセッターは、ViewModelのObservableCollectionを反復処理し、各アイテムViewModelにboolean FilteredOutプロパティを設定するFilterListメソッドを呼び出します。

フィルターテキストが変更されるとFilteredOutプロパティが更新されますが、リストは更新されません。 CollectionViewSourceフィルターイベントは、ユーザーコントロールを切り替えて再度切り替えることにより、ユーザーコントロールを再読み込みしたときにのみ発生します。

フィルター情報を更新した後にOnPropertyChanged("AllProjects")を呼び出してみましたが、問題は解決しませんでした。 (「AllProjects」は、ObservableCollectionがバインドされるViewModelのCollectionViewSourceプロパティです。)

FilterText CollectionViewSourceの値が変更されたときに、TextBoxを取得して自分自身を再フィルタリングするにはどうすればよいですか?

どうもありがとう

44
Pieter Müller

ビューにCollectionViewSourceを作成しないでください。代わりに、ビューモデルにICollectionView型のプロパティを作成し、それに_ListView.ItemsSource_をバインドします。

これを実行したら、ユーザーが変更するたびにFilterTextRefresh()を呼び出すICollectionViewプロパティのセッターにロジックを配置できます。

これにより、並べ替えの問題も簡素化されることがわかります。並べ替えロジックをビューモデルに組み込み、ビューで使用できるコマンドを公開できます。

[〜#〜] edit [〜#〜]

以下は、MVVMを使用したコレクションビューの動的な並べ替えとフィルタリングの非常に簡単なデモです。このデモはFilterTextを実装していませんが、すべてがどのように機能するかを理解すれば、FilterTextプロパティと、ハードプロパティの代わりにそのプロパティを使用する述語を実装することに問題はないはずです。現在使用しているコード化されたフィルター。

(ここでのビューモデルクラスはプロパティ変更通知を実装しないことにも注意してください。これはコードを単純にするためです。このデモではプロパティ値を実際に変更しないため、プロパティ変更通知は必要ありません。)

最初にアイテムのクラス:

_public class ItemViewModel
{
    public string Name { get; set; }
    public int Age { get; set; }
}
_

次に、アプリケーションのビューモデル。ここでは3つのことが行われています。最初に、独自のICollectionViewを作成してデータを取り込みます。次に、ビューがソートおよびフィルタリングコマンドを実行するために使用するApplicationCommand(以下を参照)を公開し、最後に、ビューをソートまたはフィルタリングするExecuteメソッドを実装します。

_public class ApplicationViewModel
{
    public ApplicationViewModel()
    {
        Items.Add(new ItemViewModel { Name = "John", Age = 18} );
        Items.Add(new ItemViewModel { Name = "Mary", Age = 30} );
        Items.Add(new ItemViewModel { Name = "Richard", Age = 28 } );
        Items.Add(new ItemViewModel { Name = "Elizabeth", Age = 45 });
        Items.Add(new ItemViewModel { Name = "Patrick", Age = 6 });
        Items.Add(new ItemViewModel { Name = "Philip", Age = 11 });

        ItemsView = CollectionViewSource.GetDefaultView(Items);
    }

    public ApplicationCommand ApplicationCommand
    {
        get { return new ApplicationCommand(this); }
    }

    private ObservableCollection<ItemViewModel> Items = 
                                     new ObservableCollection<ItemViewModel>();

    public ICollectionView ItemsView { get; set; }

    public void ExecuteCommand(string command)
    {
        ListCollectionView list = (ListCollectionView) ItemsView;
        switch (command)
        {
            case "SortByName":
                list.CustomSort = new ItemSorter("Name") ;
                return;
            case "SortByAge":
                list.CustomSort = new ItemSorter("Age");
                return;
            case "ApplyFilter":
                list.Filter = new Predicate<object>(x => 
                                                  ((ItemViewModel)x).Age > 21);
                return;
            case "RemoveFilter":
                list.Filter = null;
                return;
            default:
                return;
        }
    }
}
_

サックの種類をソートします。 IComparerを実装する必要があります:

_public class ItemSorter : IComparer
{
    private string PropertyName { get; set; }

    public ItemSorter(string propertyName)
    {
        PropertyName = propertyName;    
    }
    public int Compare(object x, object y)
    {
        ItemViewModel ix = (ItemViewModel) x;
        ItemViewModel iy = (ItemViewModel) y;

        switch(PropertyName)
        {
            case "Name":
                return string.Compare(ix.Name, iy.Name);
            case "Age":
                if (ix.Age > iy.Age) return 1;
                if (iy.Age > ix.Age) return -1;
                return 0;
            default:
                throw new InvalidOperationException("Cannot sort by " + 
                                                     PropertyName);
        }
    }
}
_

ビューモデルでExecuteメソッドをトリガーするには、ApplicationCommandクラスを使用します。これは、ICommandのボタンをCommandParameterにルーティングする単純な実装ですビューモデルのExecuteメソッドへのビュー。アプリケーションビューモデルにRelayCommandプロパティの束を作成したくなかったので、この方法で実装しました。そして、すべてのソート/フィルタリングを1つのメソッドに保持して、完了しました。

_public class ApplicationCommand : ICommand
{
    private ApplicationViewModel _ApplicationViewModel;

    public ApplicationCommand(ApplicationViewModel avm)
    {
        _ApplicationViewModel = avm;
    }

    public void Execute(object parameter)
    {
        _ApplicationViewModel.ExecuteCommand(parameter.ToString());
    }

    public bool CanExecute(object parameter)
    {
        return true;
    }

    public event EventHandler CanExecuteChanged;
}
_

最後に、アプリケーションのMainWindowを示します。

_<Window x:Class="CollectionViewDemo.MainWindow"
        xmlns="http://schemas.Microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.Microsoft.com/winfx/2006/xaml"
        xmlns:CollectionViewDemo="clr-namespace:CollectionViewDemo" 
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <CollectionViewDemo:ApplicationViewModel />
    </Window.DataContext>
    <DockPanel>
        <ListView ItemsSource="{Binding ItemsView}">
            <ListView.View>
                <GridView>
                    <GridViewColumn DisplayMemberBinding="{Binding Name}"
                                    Header="Name" />
                    <GridViewColumn DisplayMemberBinding="{Binding Age}" 
                                    Header="Age"/>
                </GridView>
            </ListView.View>
        </ListView>
        <StackPanel DockPanel.Dock="Right">
            <Button Command="{Binding ApplicationCommand}" 
                    CommandParameter="SortByName">Sort by name</Button>
            <Button Command="{Binding ApplicationCommand}" 
                    CommandParameter="SortByAge">Sort by age</Button>
            <Button Command="{Binding ApplicationCommand}"
                    CommandParameter="ApplyFilter">Apply filter</Button>
            <Button Command="{Binding ApplicationCommand}"
                    CommandParameter="RemoveFilter">Remove filter</Button>
        </StackPanel>
    </DockPanel>
</Window>
_
70
Robert Rossney

現在、多くの場合、明示的に更新をトリガーする必要はありません。 CollectionViewSourceは、ICollectionViewLiveShapingコレクションのフィールドに基づいて、IsLiveFilteringRequestedがtrueの場合に自動的に更新される LiveFilteringProperties を実装します。

XAMLの例:

  <CollectionViewSource
         Source="{Binding Items}"
         Filter="FilterPredicateFunction"
         IsLiveFilteringRequested="True">
    <CollectionViewSource.LiveFilteringProperties>
      <system:String>FilteredProperty1</system:String>
      <system:String>FilteredProperty2</system:String>
    </CollectionViewSource.LiveFilteringProperties>
  </CollectionViewSource>
23
Drew Noakes

おそらくあなたはあなたの質問でビューを単純化しましたが、書かれているように、CollectionViewSourceは本当に必要ありません-ViewModelでフィルタリングされたリストに直接バインドできます(mItemsToFilterはフィルタリングされているコレクション、おそらくあなたの例):

public ReadOnlyObservableCollection<ItemsToFilter> AllFilteredItems
{
    get 
    { 
        if (String.IsNullOrEmpty(mFilterText))
            return new ReadOnlyObservableCollection<ItemsToFilter>(mItemsToFilter);

        var filtered = mItemsToFilter.Where(item => item.Text.Contains(mFilterText));
        return new ReadOnlyObservableCollection<ItemsToFilter>(
            new ObservableCollection<ItemsToFilter>(filtered));
    }
}

public string FilterText
{
    get { return mFilterText; }
    set 
    { 
        mFilterText = value;
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("FilterText"));
            PropertyChanged(this, new PropertyChangedEventArgs("AllFilteredItems"));
        }
    }
}

ビューは次のようになります。

<TextBox Text="{Binding Path=FilterText,UpdateSourceTrigger=PropertyChanged}" />
<ListView ItemsSource="{Binding AllFilteredItems}" />

簡単なメモ:

  • これにより、コードビハインドのイベントが排除されます。

  • また、人工的なGUI専用のプロパティである "FilterOut"プロパティも削除されるため、MVVMが実際に破損します。これをシリアル化する予定がない限り、ViewModelには必要ありませんし、Modelには必要ありません。

  • この例では、「フィルターアウト」ではなく「フィルターイン」を使用します。 (ほとんどの場合)私が適用しているフィルターは、私がdoを見たいものであるということは、私にとってより論理的なようです。本当にフィルターで除外したい場合は、Contains句を無効にします(つまり、item =>!Item.Text.Contains(...))。

  • ViewModelでセットをより集中的に行う方法があるかもしれません。覚えておくべき重要なことは、FilterTextを変更するときは、AllFilteredItemsコレクションにも通知する必要があるということです。ここではインラインで行いましたが、e.PropertyNameがFilterTextの場合、PropertyChangedイベントを処理してPropertyChangedを呼び出すこともできます。

明確化が必要な場合はお知らせください。

5
Wonko the Sane
CollectionViewSource.View.Refresh();

CollectionViewSource.Filterはこの方法で再評価されます!

4
tuxy42

この問題に対するより洗練された解決策を発見しました。 代わりにViewModelにICollectionViewを作成し(受け入れられた答えが示唆するように)、バインディングを

_ItemsSource={Binding Path=YourCollectionViewSourceProperty}
_

より良い方法は、ViewModelでCollectionViewSourceプロパティを作成することです。次に、次のようにItemsSourceをバインドします

_ItemsSource={Binding Path=YourCollectionViewSourceProperty.View}    
_

。Viewが追加されていることに注意してください。このようにすると、ItemsSourceが変更されるたびにCollectionViewSourceバインディングに通知され、手動でRefresh()ICollectionView

注:なぜそうなのか判断できません。 CollectionViewSourceプロパティに直接バインドすると、バインドは失敗します。ただし、XAMLファイルのCollectionViewSource要素でResourcesを定義し、リソースキーに直接バインドする場合、バインディングは正常に機能します。私が推測できる唯一のことは、XAMLで完全に実行すると、本当にCollectionViewSource.View値にバインドすることを認識し、それをバックグラウンドでバインドすることを知っているということです(とても便利です!:/)。

1
MoMo

あなたが尋ねていることをよく理解している場合:

FilterTextプロパティの設定部分で、CollectionViewに対してRefresh()を呼び出すだけです。

1
Dummy01