web-dev-qa-db-ja.com

WPF:データバインドされたリストボックスでのユーザー選択をキャンセルしますか?

データバインドされたWPFリストボックスでのユーザー選択をキャンセルするにはどうすればよいですか?ソースプロパティは正しく設定されていますが、リストボックスの選択が同期していません。

特定の検証条件が失敗した場合にWPFリストボックスでのユーザー選択をキャンセルする必要があるMVVMアプリがあります。検証は、[送信]ボタンではなく、リストボックスでの選択によってトリガーされます。

ListBox.SelectedItemプロパティはViewModel.CurrentDocumentプロパティにバインドされています。検証が失敗した場合、ビューモデルプロパティのセッターはプロパティを変更せずに終了します。したがって、ListBox.SelectedItemがバインドされているプロパティは変更されません。

その場合、ビューモデルのプロパティセッターは、終了する前にPropertyChangedイベントを発生させます。これは、ListBoxを古い選択にリセットするのに十分であると私は想定していました。しかし、それは機能していません。ListBoxにはまだ新しいユーザーの選択が表示されます。その選択をオーバーライドして、ソースプロパティと同期させる必要があります。

明確でない場合に備えて、次に例を示します。ListBoxには、Document1とDocument2の2つの項目があります。 Document1が選択されています。ユーザーはDocument2を選択しますが、Document1は検証に失敗します。 ViewModel.CurrentDocumentプロパティは引き続きDocument1に設定されていますが、ListBoxはDocument2が選択されていることを示しています。 ListBoxの選択をDocument1に戻す必要があります。

これが私のリストボックスバインディングです:

<ListBox 
    ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
    SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />

ViewModel(イベントとして)からView(イベントをサブスクライブする)へのコールバックを使用して、SelectedItemプロパティを強制的に古い選択に戻してみました。古いドキュメントをイベントとともに渡します。これは正しいドキュメント(古い選択)ですが、リストボックスの選択は元に戻りません。

では、ListBoxの選択を、そのSelectedItemプロパティがバインドされているビューモデルプロパティと同期させるにはどうすればよいですか?ご協力いただきありがとうございます。

25
David Veeneman

-をちょきちょきと切る-

私が上で書いたことをよく忘れてください。

私は実験をしたばかりですが、実際、セッターでさらに凝ったことをすると、SelectedItemは同期しなくなります。セッターが戻るのを待ってから、ViewModelのプロパティを非同期に戻す必要があると思います。

MVVM Lightヘルパーを使用した迅速で汚い作業ソリューション(私の単純なプロジェクトでテスト済み):セッターで、CurrentDocumentの以前の値に戻す

                var dp = DispatcherHelper.UIDispatcher;
                if (dp != null)
                    dp.BeginInvoke(
                    (new Action(() => {
                        currentDocument = previousDocument;
                        RaisePropertyChanged("CurrentDocument");
                    })), DispatcherPriority.ContextIdle);

基本的に、UIスレッドでプロパティの変更をキューに入れます。ContextIdleの優先度により、UIが一貫した状態になるまで待機します。 WPFのイベントハンドラー内では、依存関係のプロパティを自由に変更できないようです。

残念ながら、それはあなたのビューモデルとあなたのビューの間に結合を作り、それは醜いハックです。

DispatcherHelper.UIDispatcherを機能させるには、最初にDispatcherHelper.Initialize()を実行する必要があります。

8
majocha

この質問に関する将来のつまずきのために、このページは最終的に私のために働いたものです: http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound- wpf-combo-box.aspx

これはコンボボックス用ですが、リストボックスでも問題なく機能します。MVVMでは、どのタイプのコントロールがセッターを呼び出しているかを気にしないためです。著者が述べているように、輝かしい秘密は、実際に基になる値を変更してから元に戻すことです。これを実行することも重要でした。別のディスパッチャ操作で「元に戻す」。

private Person _CurrentPersonCancellable;
public Person CurrentPersonCancellable
{
    get
    {
        Debug.WriteLine("Getting CurrentPersonCancellable.");
        return _CurrentPersonCancellable;
    }
    set
    {
        // Store the current value so that we can 
        // change it back if needed.
        var origValue = _CurrentPersonCancellable;

        // If the value hasn't changed, don't do anything.
        if (value == _CurrentPersonCancellable)
            return;

        // Note that we actually change the value for now.
        // This is necessary because WPF seems to query the 
        //  value after the change. The combo box
        // likes to know that the value did change.
        _CurrentPersonCancellable = value;

        if (
            MessageBox.Show(
                "Allow change of selected item?", 
                "Continue", 
                MessageBoxButton.YesNo
            ) != MessageBoxResult.Yes
        )
        {
            Debug.WriteLine("Selection Cancelled.");

            // change the value back, but do so after the 
            // UI has finished it's current context operation.
            Application.Current.Dispatcher.BeginInvoke(
                    new Action(() =>
                    {
                        Debug.WriteLine(
                            "Dispatcher BeginInvoke " + 
                            "Setting CurrentPersonCancellable."
                        );

                        // Do this against the underlying value so 
                        //  that we don't invoke the cancellation question again.
                        _CurrentPersonCancellable = origValue;
                        OnPropertyChanged("CurrentPersonCancellable");
                    }),
                    DispatcherPriority.ContextIdle,
                    null
                );

            // Exit early. 
            return;
        }

        // Normal path. Selection applied. 
        // Raise PropertyChanged on the field.
        Debug.WriteLine("Selection applied.");
        OnPropertyChanged("CurrentPersonCancellable");
    }
}

注:作成者は、変更を元に戻すアクションのContextIdleDispatcherPriorityを使用します。これは問題ありませんが、Renderよりも優先度が低くなります。つまり、選択したアイテムが一時的に変更されて元に戻るときに、変更がUIに表示されます。ディスパッチャの優先度NormalまたはSend(最高の優先度)を使用すると、変更の表示が優先されます。これが私がやったことです。 DispatcherPriority列挙の詳細については、ここを参照してください。

42
Aphex

とった!彼の答えの下にある彼のコメントが私を解決に導いたので、私はマジョチャの答えを受け入れるつもりです。

これが私がしたwnatです:私はコードビハインドでListBoxのSelectionChangedイベントハンドラーを作成しました。はい、それは醜いですが、それは動作します。コードビハインドには、-1に初期化されるモジュールレベルの変数_m_OldSelectedIndex_も含まれています。 SelectionChangedハンドラーは、ViewModelのValidate()メソッドを呼び出し、ドキュメントが有効かどうかを示すブール値を返します。ドキュメントが有効な場合、ハンドラーは_m_OldSelectedIndex_を現在の_ListBox.SelectedIndex_に設定して終了します。ドキュメントが無効な場合、ハンドラーは_ListBox.SelectedIndex_を_m_OldSelectedIndex_にリセットします。イベントハンドラのコードは次のとおりです。

_private void OnSearchResultsBoxSelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var viewModel = (MainViewModel) this.DataContext;
    if (viewModel.Validate() == null)
    {
        m_OldSelectedIndex = SearchResultsBox.SelectedIndex;
    }
    else
    {
        SearchResultsBox.SelectedIndex = m_OldSelectedIndex;
    }
}
_

この解決策にはトリックがあることに注意してください。SelectedIndexプロパティを使用する必要があります。 SelectedItemプロパティでは機能しません。

Majochaを助けてくれてありがとう、そしてうまくいけば、これは将来誰かを助けるでしょう。私のように、今から半年後、この解決策を忘れてしまったとき...

6
David Veeneman

.NET 4.5では、バインディングに遅延フィールドが追加されました。遅延を設定すると、自動的に更新を待機するため、ViewModelにディスパッチャーは必要ありません。これは、ListBoxおよびComboBoxのSelectedItemプロパティなどのすべてのSelector要素の検証に機能します。遅延はミリ秒単位です。

<ListBox 
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, Delay=10}" />
4
bwing

MVVMのフォローを真剣に考えていて、コードの背後にコードを置きたくない場合や、率直に言ってエレガントでもないDispatcherの使用も気に入らない場合は、次の解決策が役に立ちます。ここで提供されるほとんどのソリューションよりもエレガントです。

これは、背後のコードでSelectionChangedイベントを使用して選択を停止できるという概念に基づいています。さて、これが当てはまる場合は、その動作を作成して、コマンドをSelectionChangedイベントに関連付けてみませんか。ビューモデルでは、前に選択したインデックスと現在選択したインデックスを簡単に思い出すことができます。秘訣は、SelectedIndexでビューモデルにバインドし、選択が変更されるたびにビューモデルを変更することです。しかし、選択が実際に変更された直後に、SelectionChangedイベントが発生し、コマンドを介してビューモデルに通知されるようになりました。以前に選択したインデックスを覚えているので、それを検証できます。正しくない場合は、選択したインデックスを元の値に戻します。

動作のコードは次のとおりです。

public class ListBoxSelectionChangedBehavior : Behavior<ListBox>
{
    public static readonly DependencyProperty CommandProperty 
        = DependencyProperty.Register("Command",
                                     typeof(ICommand),
                                     typeof(ListBoxSelectionChangedBehavior), 
                                     new PropertyMetadata());

    public static DependencyProperty CommandParameterProperty
        = DependencyProperty.Register("CommandParameter",
                                      typeof(object), 
                                      typeof(ListBoxSelectionChangedBehavior),
                                      new PropertyMetadata(null));

    public ICommand Command
    {
        get { return (ICommand)GetValue(CommandProperty); }
        set { SetValue(CommandProperty, value); }
    }

    public object CommandParameter
    {
        get { return GetValue(CommandParameterProperty); }
        set { SetValue(CommandParameterProperty, value); }
    }

    protected override void OnAttached()
    {
        AssociatedObject.SelectionChanged += ListBoxOnSelectionChanged;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.SelectionChanged -= ListBoxOnSelectionChanged;
    }

    private void ListBoxOnSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        Command.Execute(CommandParameter);
    }
}

XAMLでの使用:

<ListBox x:Name="ListBox"
         Margin="2,0,2,2"
         ItemsSource="{Binding Taken}"
         ItemContainerStyle="{StaticResource ContainerStyle}"
         ScrollViewer.HorizontalScrollBarVisibility="Disabled"
         HorizontalContentAlignment="Stretch"
         SelectedIndex="{Binding SelectedTaskIndex, Mode=TwoWay}">
    <i:Interaction.Behaviors>
        <b:ListBoxSelectionChangedBehavior Command="{Binding SelectionChangedCommand}"/>
    </i:Interaction.Behaviors>
</ListBox>

ビューモデルで適切なコードは次のとおりです。

public int SelectedTaskIndex
{
    get { return _SelectedTaskIndex; }
    set { SetProperty(ref _SelectedTaskIndex, value); }
}

private void SelectionChanged()
{
    if (_OldSelectedTaskIndex >= 0 && _SelectedTaskIndex != _OldSelectedTaskIndex)
    {
        if (Taken[_OldSelectedTaskIndex].IsDirty)
        {
            SelectedTaskIndex = _OldSelectedTaskIndex;
        }
    }
    else
    {
        _OldSelectedTaskIndex = _SelectedTaskIndex;
    }
}

public RelayCommand SelectionChangedCommand { get; private set; }

ビューモデルのコンストラクター:

SelectionChangedCommand = new RelayCommand(SelectionChanged);

RelayCommandはMVVMライトの一部です。あなたがそれを知らないならば、それをグーグルしてください。あなたは参照する必要があります

xmlns:i="http://schemas.Microsoft.com/expression/2010/interactivity"

したがって、System.Windows.Interactivityを参照する必要があります。

3
Philip Stuyck

私は最近これに直面し、コードを必要とせずに、MVVMでうまく機能するソリューションを思いつきました。

モデルにSelectedIndexプロパティを作成し、リストボックスSelectedIndexをそれにバインドしました。

View CurrentChangingイベントで、検証を行います。失敗した場合は、コードを使用するだけです。

e.cancel = true;

//UserView is my ICollectionView that's bound to the listbox, that is currently changing
SelectedIndex = UserView.CurrentPosition;  

//Use whatever similar notification method you use
NotifyPropertyChanged("SelectedIndex"); 

それは完全にATMで動作するようです。エッジの場合はそうではないかもしれませんが、今のところ、私が望むことを正確に実行します。

1
JLCNZ

ListBoxのプロパティをバインドします:IsEnabled="{Binding Path=Valid, Mode=OneWay}"ここで、Validは、検証アルゴリズムを使用したビューモデルプロパティです。他の解決策は、私の目には行き過ぎに見えます。

無効化された外観が許可されていない場合、スタイルが役立つ可能性がありますが、選択を変更することは許可されていないため、おそらく無効化されたスタイルは問題ありません。

たぶん.NETバージョン4.5ではINotifyDataErrorInfoが役に立ちます、私にはわかりません。

0
Gerard

非常によく似た問題がありましたが、違いは、ListViewICollectionViewにバインドして使用していて、IsSynchronizedWithCurrentItemプロパティをバインドするのではなくSelectedItemを使用していたことです。 ListView。これは、基になるCurrentItemChangedICollectionViewイベントをキャンセルするまではうまく機能し、ListView.SelectedItemICollectionView.CurrentItemと同期していませんでした。

ここでの根本的な問題は、ビューをビューモデルと同期させることです。明らかに、ビューモデルで選択変更要求をキャンセルするのは簡単です。ですから、私に関する限り、より応答性の高いビューが本当に必要です。 ListView同期の制限を回避するために、ViewModelにクラッジを入れないようにします。一方で、ビューコードビハインドにビュー固有のロジックを追加できてうれしいです。

したがって、私の解決策は、コードビハインドでのListView選択のために独自の同期を配線することでした。私に関する限り、完全にMVVMであり、ListViewIsSynchronizedWithCurrentItemのデフォルトよりも堅牢です。

これが私の背後にあるコードです...これにより、ViewModelから現在のアイテムを変更することもできます。ユーザーがリストビューをクリックして選択を変更すると、すぐに変更され、下流の何かが変更をキャンセルした場合は元に戻ります(これは私の望ましい動作です)。 IsSynchronizedWithCurrentItemListViewをfalseに設定していることに注意してください。また、ここではasync/awaitを使用していますが、これはうまく機能しますが、awaitが戻ったときに、同じデータコンテキストにいることを少し再確認する必要があります。 。

void DataContextChangedHandler(object sender, DependencyPropertyChangedEventArgs e)
{
    vm = DataContext as ViewModel;
    if (vm != null)
        vm.Items.CurrentChanged += Items_CurrentChanged;
}

private async void myListView_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    var vm = DataContext as ViewModel; //for closure before await
    if (vm != null)
    {
        if (myListView.SelectedIndex != vm.Items.CurrentPosition)
        {
            var changed = await vm.TrySetCurrentItemAsync(myListView.SelectedIndex);
            if (!changed && vm == DataContext)
            {
                myListView.SelectedIndex = vm.Items.CurrentPosition; //reset index
            }
        }
    }
}

void Items_CurrentChanged(object sender, EventArgs e)
{
    var vm = DataContext as ViewModel; 
    if (vm != null)
        myListView.SelectedIndex = vm.Items.CurrentPosition;
}

次に、ViewModelクラスにICollectionViewという名前のItemsとこのメソッドがあります(簡略化されたバージョンが表示されます)。

public async Task<bool> TrySetCurrentItemAsync(int newIndex)
{
    DataModels.BatchItem newCurrentItem = null;
    if (newIndex >= 0 && newIndex < Items.Count)
    {
        newCurrentItem = Items.GetItemAt(newIndex) as DataModels.BatchItem;
    }

    var closingItem = Items.CurrentItem as DataModels.BatchItem;
    if (closingItem != null)
    {
        if (newCurrentItem != null && closingItem == newCurrentItem)
            return true; //no-op change complete

        var closed = await closingItem.TryCloseAsync();

        if (!closed)
            return false; //user said don't change
    }

    Items.MoveCurrentTo(newCurrentItem);
    return true; 
}

TryCloseAsyncの実装では、ある種のダイアログサービスを使用して、ユーザーからの綿密な確認を引き出すことができます。

0
TCC