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

Dav*_*man 24 wpf mvvm wpf-controls

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

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

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

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

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

这是我的ListBox绑定:

<ListBox 
    ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
    SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Run Code Online (Sandbox Code Playgroud)

我尝试使用从ViewModel(作为事件)到View(订阅事件)的回调,强制将SelectedItem属性返回到旧选择.我使用事件传递旧文档,它是正确的(旧选择),但ListBox选择不会更改回来.

那么,如何将ListBox选项与其SelectedItem属性绑定的视图模型属性同步返回?谢谢你的帮助.

Aph*_*hex 38

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

这是一个组合框,但适用于列表框就好了,因为在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");
    }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我是个傻瓜,这正是我想要的.我唯一要补充的是你需要检查`Application.Current`是否为单元测试为空并相应处理. (4认同)
  • 对于某些类型的项目,Application.Current.Dispatcher可以为null ...而是使用Dispatcher.CurrentDispatcher. (2认同)
  • 添加延迟似乎可以让用户界面赶上字段设置被绕过的事实。SelectedItem="{绑定employees_vm.current_employee,Mode=TwoWay,UpdateSourceTrigger=PropertyChanged,Delay=1}" (2认同)

bwi*_*ing 12

在 .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}" />
Run Code Online (Sandbox Code Playgroud)


maj*_*cha 8

-snip-

好吧忘记我上面写的内容.

我刚做了一个实验,而且只要你在setter中做了更多花哨的事情,SelectedItem确实会失去同步.我想你需要等待setter返回,然后异步更改ViewModel中的属性.

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

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

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

不幸的是,它会在您的视图模型和视图之间创建耦合,这是一个丑陋的黑客攻击.

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

  • 更优雅的解决方案是在viewmodel上添加IsCurrentDocumentValid属性或只是一个Validate()方法,并在视图中使用它来允许或禁止选择更改. (2认同)

Dav*_*man 6

得到它了!我将接受majocha的回答,因为他在答案之下的评论让我得到了解决方案.

这是我做的wnat:我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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

感谢您的帮助majocha,希望这将有助于其他人在路上.像我一样,六个月后,我忘记了这个解决方案......


Phi*_*yck 5

如果您认真遵循 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

在 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>
Run Code Online (Sandbox Code Playgroud)

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

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; }
Run Code Online (Sandbox Code Playgroud)

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

SelectionChangedCommand = new RelayCommand(SelectionChanged);
Run Code Online (Sandbox Code Playgroud)

RelayCommand是 MVVM 光的一部分。如果你不知道的话谷歌一下。您需要参考

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Run Code Online (Sandbox Code Playgroud)

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