Bell’s diary

WPFやC#の小ネタ、その他いろいろ気まぐれで投稿してみる予定。

CollectionViewSourceのFilter条件を変更するAction

WPFのListBoxなどは、直接ObservableCollectionなどへバインドするのではなく、XAML上でCollectionViewSourceをResourceとして定義して、それを経由してバインドすることができます。
こうしておくと、CollectionViewSourceのソート条件やフィルタ条件を変更することで、表示する対象やその並び順を変更することができます。
このフィルタ条件の変更を、LivetのInteractionMessageActionとして作成した例です。

まず、Action本体。
フィルタ条件変更後の表示件数をMessageのResponseとして返します。

using System.Linq;
using System.Windows;
using System.Windows.Data;
using Livet.Behaviors.Messaging;
using Livet.EventListeners.WeakEvents;
using Livet.Messaging;

namespace Bells.Behaviors
{
    /// <summary>
    /// CollectionViewSourceのFilterの内容を変更し、同時にその表示内容に更新するAction 
    /// </summary>
    public class SetCollectionViewSourceFilterAction : InteractionMessageAction<Window>
    {
        private LivetWeakEventListener<FilterEventHandler, FilterEventArgs> _listener; 

        protected override void InvokeAction(InteractionMessage m)
        {
            var msg = m as FilterMessage;
            if (msg == null) return;

            var cvs = (CollectionViewSource)(AssociatedObject.FindResource(msg.CollectionViewSourceKey));
                
            _listener = new LivetWeakEventListener<FilterEventHandler, FilterEventArgs>
                (
                h => new FilterEventHandler(h),
                h => cvs.Filter += h,
                h => cvs.Filter -= h,
                (sender,e)=> msg.FilterEventHandler(sender,e)
                );

            cvs.IsLiveFilteringRequested = true;
            cvs.IsLiveFilteringRequested = false;
            msg.Response = cvs.View.Cast<object>().Count();
        }
    }
}

上のActionに渡すMessageがこちら。

using System.Windows;
using System.Windows.Data;
using Livet.Messaging;

namespace Bells.Behaviors
{
    /// <summary>
    /// Filter変更用のメッセージ。Responseはint型でフィルター適用後の件数を受け取る。
    /// </summary>
    public class FilterMessage : ResponsiveInteractionMessage<int>
    {
        public FilterMessage()
        {
        }

        public FilterMessage(string messageKey): base(messageKey)
        {
        }

        public FilterMessage(string collectionViewSourceKey, FilterEventHandler filterEventHandler, string messageKey)
            : this(messageKey)
        {
            CollectionViewSourceKey = collectionViewSourceKey;
            FilterEventHandler = filterEventHandler;
            _msgKey = messageKey;
        }

        private readonly string _msgKey;
        public string CollectionViewSourceKey { get; set; }
        public FilterEventHandler FilterEventHandler { get; set; }

        protected override Freezable CreateInstanceCore()
        {
            return new FilterMessage(CollectionViewSourceKey, FilterEventHandler, _msgKey);
        }
    }
}

これを利用するViewのXAMLでは、まず、

<Window.Resources>
  <CollectionViewSource Source="{Binding BellCollection}" x:Key="BellSource1"/>
</Window.Resources>

などとResourceの中にCollectionViewSourceを定義し、そちらからViewModelの本来のバインドすべき大元のCollectionなどへバインドしておき、さらに、

<ListBox ItemsSource="{Binding Source={StaticResource BellSource1}}"/>

と、表示するListBoxなどからはこのCollectionViewSourceをItemsSourceに指定しておきます。

また、ViewModelからMessageが投げられたときに肝心のActionを動作させるために

<i:Interaction.Triggers>
  <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="SetFilter">
    <behaviors:SetCollectionViewSourceFilterAction/>
  </l:InteractionMessageTrigger>
</i:Interaction.Triggers>

の記述も必要です。View側は以上。

続いて、実際にこのActionをViewModelから呼ぶためには、まずFilterイベントハンドラを作っておく必要があります。

private void Filter(object sender, FilterEventArgs e)
{
	//e.Item にobject型でCollectionViewsSourceの各行のレコードが入ってくるので(レコード全件と同じ回数呼ばれます)
	//元の型にキャストして、そのプロパティなどを元に判断し、表示するかしないか決定します。
	//e.Accepted をtrue/falseに設定するかでその行の表示/非表示を決定してreturn。
}

で、実際に、何かボタンを押した時にフィルタ条件を変更する場合には、

public void Search()
{
    var msg = new FilterMessage("BellSource1", Filter, "SetFilter");
    Messenger.Raise(msg);
    if (msg.Response == 0)
        Messenger.Raise(new InformationMessage("該当レコードなし", "該当レコードなし", MessageBoxImage.Information, "Info"));
}

こんな感じになりますね。フィルタ条件変更後、1件以上何か表示されるならそれで終了。条件に合うレコードが0件で何も表示されない時だけメッセージボックスも表示しています。