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

时间:2010-04-09 14:01:39

标签: wpf mvvm wpf-controls

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

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

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

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

以防万一不清楚,这是一个例子: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}" />

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

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

8 个答案:

答案 0 :(得分:35)

对于这个问题的未来绊脚石,这个页面最终对我有用: 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");
    }
}

注意:作者使用ContextIdleDispatcherPriority撤消更改的操作。虽然很好,但优先级低于Render,这意味着更改将在UI中显示为所选项目暂时更改和更改。使用调度程序优先级Normal或甚至Send(最高优先级)优先显示更改。这就是我最终做的事情。 See here for details about the DispatcherPriority enumeration.

答案 1 :(得分:7)

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

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

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

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

答案 2 :(得分:5)

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

以下是wnat:我在代码隐藏中为ListBox创建了一个SelectionChanged事件处理程序。是的,这很难看,但它确实有效。代码隐藏还包含一个模块级变量m_OldSelectedIndex,它被初始化为-1。 SelectionChanged处理程序调用ViewModel的Validate()方法并获取一个布尔值,指示Document是否有效。如果文档有效,则处理程序将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,希望这将有助于其他人。像我一样,六个月后,当我忘记了这个解决方案......

答案 3 :(得分:3)

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

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

行为的代码如下:

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>

viewmodel中适用的代码如下:

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

在viewmodel的构造函数中:

SelectionChangedCommand = new RelayCommand(SelectionChanged);

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

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

因此您需要引用System.Windows.Interactivity

答案 4 :(得分:2)

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

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

答案 5 :(得分:1)

我最近反对这一点,并提出了一个适用于我的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上。可能存在边缘情况,但是现在,它完全符合我的要求。

答案 6 :(得分:0)

我遇到了一个非常类似的问题,区别在于我使用ListView绑定到ICollectionView并使用IsSynchronizedWithCurrentItem而不是绑定SelectedItem属性ListView。这对我来说效果很好,直到我想取消基础CurrentItemChanged的{​​{1}}事件,这导致ICollectionViewListView.SelectedItem不同步。

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

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

这是我的代码背后......这也允许从ViewModel更改当前项目。如果用户单击列表视图并更改选择,它将立即更改,然后在下游取消更改时返回(这是我想要的行为)。注意我IsSynchronizedWithCurrentItem上的IsSynchronizedWithCurrentItem设置为false。另请注意,我在这里使用ListView / async可以很好地运行,但需要进行一些仔细检查,当await返回时,我们仍然处于相同的数据上下文中。

await

然后在我的ViewModel类中,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; } 名为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; } 的实现可以使用某种对话服务来引起用户的密切确认。

答案 7 :(得分:-1)

绑定ListBox的属性:IsEnabled="{Binding Path=Valid, Mode=OneWay}"其中Valid是带有验证算法的视图模型属性。其他解决方案在我眼中看起来太过牵强。

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

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