WPF:取消数据绑定列表框中的用户选择?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/2608071/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-08 21:40:03  来源:igfitidea点击:

WPF: Cancel a user selection in a databound ListBox?

wpfmvvmwpf-controls

提问by David Veeneman

How do I cancel a user selection in a databound WPF ListBox? The source property is set correctly, but the ListBox selection is out of sync.

如何取消数据绑定 WPF 列表框中的用户选择?源属性设置正确,但 ListBox 选择不同步。

I have an MVVM app that needs to cancel a user selection in a WPF ListBox if certain validation conditions fail. Validation is triggered by a selection in the ListBox, rather than by a Submit button.

我有一个 MVVM 应用程序,如果某些验证条件失败,它需要取消 WPF ListBox 中的用户选择。验证由列表框中的选择触发,而不是由提交按钮触发。

The ListBox.SelectedItemproperty is bound to a ViewModel.CurrentDocumentproperty. If validation fails, the setter for the view model property exits without changing the property. So, the property to which ListBox.SelectedItemis bound doesn't get changed.

ListBox.SelectedItem属性绑定到一个ViewModel.CurrentDocument属性。如果验证失败,则视图模型属性的 setter 将退出而不更改该属性。因此,ListBox.SelectedItem绑定到的属性不会改变。

If that happens, the view model property setter does raise the PropertyChanged event before it exits, which I had assumed would be enough to reset the ListBox back to the old selection. But that's not working--the ListBox still shows the new user selection. I need to override that selection and get it back in sync with the source property.

如果发生这种情况,视图模型属性设置器会在退出之前引发 PropertyChanged 事件,我认为这足以将 ListBox 重置回旧选择。但这不起作用——ListBox 仍然显示新的用户选择。我需要覆盖该选择并使其与源属性同步。

Just in case that's not clear, here is an example: The ListBox has two items, Document1 and Document2; Document1 is selected. The user selects Document2, but Document1 fails to validate. The ViewModel.CurrentDocumentproperty is still set to Document1, but the ListBox shows that Document2 is selected. I need to get the ListBox selection back to Document1.

以防万一不清楚,这里有一个例子: ListBox 有两个项目,Document1 和 Document2;文档 1 被选中。用户选择 Document2,但 Document1 无法验证。该ViewModel.CurrentDocument属性仍设置为 Document1,但 ListBox 显示已选择 Document2。我需要将 ListBox 选择返回到 Document1。

Here is my ListBox Binding:

这是我的 ListBox 绑定:

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

I did try using a callback from the ViewModel (as an event) to the View (which subscribes to the event), to force the SelectedItem property back to the old selection. I pass the old Document with the event, and it is the correct one (the old selection), but the ListBox selection doesn't change back.

我确实尝试使用从 ViewModel(作为事件)到 View(订阅事件)的回调,以强制 SelectedItem 属性回到旧的选择。我通过事件传递旧文档,它是正确的(旧选择),但列表框选择不会变回。

So, how do I get the ListBox selection back in sync with the view model property to which its SelectedItemproperty is bound? Thanks for your help.

那么,如何让 ListBox 选择与其SelectedItem属性绑定到的视图模型属性同步?谢谢你的帮助。

采纳答案by majocha

-snip-

-剪-

Well forget what I wrote above.

好吧忘记我上面写的。

I just did an experiment, and indeed SelectedItem goes out of sync whenever you do anything more fancy in the setter. I guess you need to wait for the setter to return, and then change the property back in your ViewModel asynchronously.

我刚刚做了一个实验,当你在 setter 中做任何更花哨的事情时,确实 SelectedItem 会失去同步。我猜您需要等待 setter 返回,然后异步更改 ViewModel 中的属性。

Quick and dirty working solution (tested in my simple project) using MVVM Light helpers: In your setter, to revert to previous value of CurrentDocument

使用 MVVM Light 助手的快速而肮脏的工作解决方案(在我的简单项目中测试):在您的设置器中,恢复到 CurrentDocument 的先前值

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

it basically queues the property change on the UI thread, ContextIdle priority will ensure it will wait for UI to be in consistent state. it Appears you cannot freely change dependency properties while inside event handlers in WPF.

它基本上在 UI 线程上排队属性更改,ContextIdle 优先级将确保它等待 UI 处于一致状态。它出现在 WPF 中的事件处理程序中时,您无法自由更改依赖项属性。

Unfortunately it creates coupling between your view model and your view and it's an ugly hack.

不幸的是,它会在您的视图模型和您的视图之间产生耦合,这是一个丑陋的黑客。

To make DispatcherHelper.UIDispatcher work you need to do DispatcherHelper.Initialize() first.

要使 DispatcherHelper.UIDispatcher 工作,您需要先执行 DispatcherHelper.Initialize()。

回答by Aphex

For future stumblers on this question, this page is what ultimately worked for me: http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx

对于这个问题的未来绊脚石,这个页面最终对我有用:http: //blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-框.aspx

It's for a combobox, but works for a listbox just fine, since in MVVM you don't really care what type of control is calling the setter. The glorious secret, as the author mentions, is to actually change the underlying value and then change it back.It was also important to run this “undo” on a separate dispatcher operation.

它适用于组合框,但适用于列表框就好了,因为在 MVVM 中,您并不真正关心调用 setter 的控件类型。正如作者所提到的,光荣的秘密是实际改变潜在价值,然后再将其改回来。在单独的调度程序操作上运行此“撤消”也很重要。

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");
    }
}

Note:The author uses ContextIdlefor the DispatcherPriorityfor the action to undo the change. While fine, this is a lower priority than Render, which means that the change will show in the UI as the selected item momentarily changing and changing back. Using a dispatcher priority of Normalor even Send(the highest priority) preempts the display of the change. This is what I ended up doing. See here for details about the DispatcherPriorityenumeration.

注意:作者使用ContextIdleforDispatcherPriority来撤消更改的操作。虽然很好,但它的优先级低于Render,这意味着更改将在 UI 中显示为所选项目暂时更改并更改回来。使用调度程序优先级Normal或什至Send(最高优先级)会抢占更改的显示。这就是我最终要做的。有关DispatcherPriority枚举的详细信息,请参见此处。

回答by David Veeneman

Got it! I am going to accept majocha's answer, because his comment underneath his answer led me to the solution.

知道了!我将接受 majocha 的回答,因为他在回答下面的评论使我找到了解决方案。

Here is wnat I did: I created a SelectionChangedevent handler for the ListBox in code-behind. Yes, it's ugly, but it works. The code-behind also contains a module-level variable, m_OldSelectedIndex, which is initialized to -1. The SelectionChangedhandler calls the ViewModel's Validate()method and gets a boolean back indicating whether the Document is valid. If the Document is valid, the handler sets m_OldSelectedIndexto the current ListBox.SelectedIndexand exits. If the document is invalid, the handler resets ListBox.SelectedIndexto m_OldSelectedIndex. Here is the code for the event handler:

这是我所做的:我SelectionChanged在代码隐藏中为 ListBox创建了一个事件处理程序。是的,它很丑,但它有效。代码隐藏还包含一个模块级变量,m_OldSelectedIndex初始化为 -1。该SelectionChanged处理程序调用视图模型的Validate()方法,并得到一个布尔值回指示文件是否有效。如果 Document 有效,则处理程序设置m_OldSelectedIndex为当前ListBox.SelectedIndex并退出。如果文档无效,处理程序将重置ListBox.SelectedIndexm_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;
    }
}

Note that there is a trick to this solution: You have to use the SelectedIndexproperty; it doesn't work with the SelectedItemproperty.

请注意,此解决方案有一个技巧:您必须使用该SelectedIndex属性;它不适用于该SelectedItem属性。

Thanks for your help majocha, and hopefully this will help somebody else down the road. Like me, six months from now, when I have forgotten this solution...

感谢您的帮助 majocha,希望这会帮助其他人。像我一样,六个月后,当我忘记这个解决方案时......

回答by bwing

In .NET 4.5 they added the Delay field to the Binding. If you set the delay it will automatically wait to update so there is no need for the Dispatcher in the ViewModel. This works for validation of all Selector elements like the ListBox's and ComboBox's SelectedItem properties. The Delay is in milliseconds.

在 .NET 4.5 中,他们将 Delay 字段添加到 Binding。如果您设置延迟,它将自动等待更新,因此 ViewModel 中不需要 Dispatcher。这适用于验证所有 Selector 元素,如 ListBox 和 ComboBox 的 SelectedItem 属性。延迟以毫秒为单位。

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

回答by Philip Stuyck

If you are serious about following MVVM and don't want any code behind, and also don't like the use of the Dispatcher, which frankly is not elegant either, the following solution works for me and is by far more elegant than most of the solutions provided here.

如果你认真地关注 MVVM 并且不想要任何代码,也不喜欢使用Dispatcher,坦率地说这也不优雅,以下解决方案对我有用,并且比大多数解决方案更优雅此处提供的解决方案。

It is based on the notion that in code behind you are able to stop the selection using the SelectionChangedevent. Well now, if this is the case, why not create a behavior for it, and associate a command with the SelectionChangedevent. In the viewmodel you can then easily remember the previous selected index and the current selected index. The trick is to have binding to your viewmodel on SelectedIndexand just let that one change whenever the selection changes. But immediately after the selection really has changed, the SelectionChangedevent fires which now is notified via the command to your viewmodel. Because you remember the previously selected index, you can validate it and if not correct, you move the selected index back to the original value.

它基于这样的概念,即在后面的代码中,您可以使用SelectionChanged事件停止选择。那么现在,如果是这种情况,为什么不为其创建行为,并将命令与SelectionChanged事件相关联。在视图模型中,您可以轻松记住之前选择的索引和当前选择的索引。诀窍是绑定到您的视图模型,SelectedIndex并在选择更改时让该视图模型更改。但是在选择真正改变后,SelectionChanged事件会立即触发,现在通过命令通知您的视图模型。因为您记得之前选择的索引,所以您可以对其进行验证,如果不正确,您可以将所选索引移回原始值。

The code for the behavior is as follows:

该行为的代码如下:

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);
    }
}

Using it in XAML:

在 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>

The code that is appropriate in the viewmodel is as follows:

适合在视图模型中的代码如下:

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; }

In the constructor of the viewmodel:

在视图模型的构造函数中:

SelectionChangedCommand = new RelayCommand(SelectionChanged);

RelayCommandis part of MVVM light. Google it if you don't know it. You need to refer to

RelayCommand是 MVVM 灯的一部分。如果你不知道它,谷歌它。你需要参考

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

and hence you need to reference System.Windows.Interactivity.

因此你需要参考System.Windows.Interactivity.

回答by JLCNZ

I came up against this recently, and came up with a solution that works well with my MVVM, without the need for and code behind.

我最近遇到了这个问题,并提出了一个适用于我的 MVVM 的解决方案,无需任何代码。

I created a SelectedIndex property in my model and bound the listbox SelectedIndex to it.

我在我的模型中创建了一个 SelectedIndex 属性并将列表框 SelectedIndex 绑定到它。

On the View CurrentChanging event, I do my validation, if it fails, I simply use the code

在 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"); 

It seems to work perfectly ATM. There may be edge cases where it doesnt, but for now, it does exactly what I want.

它似乎在 ATM 上工作得很好。可能存在没有的边缘情况,但就目前而言,它完全符合我的要求。

回答by TCC

I had a very similar problem, the difference being that I am using ListViewbound to an ICollectionViewand was using IsSynchronizedWithCurrentItemrather than binding the SelectedItemproperty of the ListView. This worked well for me until I wanted to cancel the CurrentItemChangedevent of the underlying ICollectionView, which left the ListView.SelectedItemout of sync with the ICollectionView.CurrentItem.

我有一个非常类似的问题,不同的是我使用ListView绑定到ICollectionView并使用IsSynchronizedWithCurrentItem,而不是绑定SelectedItem的属性ListView。这对我来说效果很好,直到我想取消CurrentItemChanged底层的事件ICollectionView,这使得ListView.SelectedItemICollectionView.CurrentItem.

The underlying problem here is keeping the view in sync with the view model. Obviously cancelling a selection change request in the view model is trivial. So we really just need a more responsive view as far as I'm concerned. I'd rather avoid putting kludges into my ViewModel to work around limitations of the ListViewsynchronization. On the other hand I'm more than happy to add some view-specific logic to my view code-behind.

这里的潜在问题是保持视图与视图模型同步。显然,取消视图模型中的选择更改请求是微不足道的。所以就我而言,我们真的只需要一个更具响应性的视图。我宁愿避免将 kludges 放入我的 ViewModel 以解决ListView同步的限制。另一方面,我非常乐意在我的视图代码隐藏中添加一些特定于视图的逻辑。

So my solution was to wire my own synchronization for the ListView selection in the code-behind. Perfectly MVVM as far as I'm concerned and more robust than the default for ListViewwith IsSynchronizedWithCurrentItem.

所以我的解决方案是在代码隐藏中为 ListView 选择连接我自己的同步。就我而言,完美的 MVVM 比ListViewwith的默认值更强大IsSynchronizedWithCurrentItem

Here is my code behind ... this allows changing the current item from the ViewModel as well. If the user clicks the list view and changes the selection, it will immediately change, then change back if something down-stream cancels the change (this is my desired behavior). Note I have IsSynchronizedWithCurrentItemset to false on the ListView. Also note that I am using async/awaithere which plays nicely, but requires a little double-checking that when the awaitreturns, we are still in the same data context.

这是我背后的代码......这也允许从 ViewModel 更改当前项目。如果用户单击列表视图并更改选择,它将立即更改,如果下游取消更改(这是我想要的行为),则更改回来。注意我IsSynchronizedWithCurrentItemListView. 另请注意,我在这里使用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;
}

Then in my ViewModel class I have ICollectionViewnamed Itemsand this method (a simplified version is presented).

然后在我的 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; 
}

The implementation of TryCloseAsynccould use some kind of dialog service to elicit a close confirmation from the user.

的实现TryCloseAsync可以使用某种对话服务来引起用户的关闭确认。

回答by Gerard

Bind ListBox's property: IsEnabled="{Binding Path=Valid, Mode=OneWay}"where Validis the view-model property with the validation algoritm. Other solutions look too far-fetched in my eyes.

BindListBox的属性:带有验证算法的视图模型属性IsEnabled="{Binding Path=Valid, Mode=OneWay}"在哪里Valid。其他解决方案在我看来太牵强了。

When the disabled appearance is not allowed, a style could help out, but probably the disabled style is ok because changing the selection is not allowed.

当不允许禁用外观时,样式可能会有所帮助,但禁用样式可能没问题,因为不允许更改选择。

Maybe in .NET version 4.5 INotifyDataErrorInfo helps, I dont'know.

也许在 .NET 4.5 版中 INotifyDataErrorInfo 有帮助,我不知道。