代码之家  ›  专栏  ›  技术社区  ›  David Veeneman

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

  •  24
  • David Veeneman  · 技术社区  · 14 年前

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

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

    这个 ListBox.SelectedItem 属性绑定到 ViewModel.CurrentDocument 属性。如果验证失败,则视图模型属性的setter将退出,而不更改该属性。所以,财产 列表框.selecteditem 绑定不变。

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

    如果不清楚,这里有一个例子:列表框有两个项目,document1和document2;document1被选中。用户选择document2,但document1无法验证。这个 视图模型.当前文档 属性仍设置为document1,但列表框显示已选中document2。我需要将列表框选择返回到document1。

    这是我的列表框绑定:

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

    我确实尝试使用一个从视图模型(作为事件)到视图(订阅事件)的回调,以强制selectedItem属性返回到旧的选择。我将旧文档与事件一起传递,它是正确的文档(旧选择),但列表框选择不会更改回。

    那么,如何使列表框选择与它的视图模型属性恢复同步? SelectedItem 属性是否绑定?谢谢你的帮助。

    8 回复  |  直到 5 年前
        1
  •  7
  •   majocha    14 年前

    -剪断-

    好吧,忘了我上面写的。

    我刚刚做了一个实验,实际上每当你在setter中做任何更有趣的事情时,selecteditem都会失去同步。我猜您需要等待setter返回,然后异步地在您的viewModel中更改该属性。

    使用MVVM Light Helpers的快速和肮脏的工作解决方案(在我的简单项目中测试): 在setter中,恢复到当前文档的前一个值

                    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
  •  35
  •   Michael Myers KitsuneYMG    9 年前

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

    它适用于组合框,但适用于列表框,因为在MVVM中,您并不真正关心调用setter的控件类型。正如作者所说,光荣的秘密是 实际上,更改基础值,然后再将其更改回来。 在单独的调度程序操作上运行这个__undo_157;也很重要。

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

    注: 作者使用 ContextIdle 对于 DispatcherPriority 用于撤消更改的操作。虽然很好,但这比 Render ,这意味着更改将在用户界面中显示为所选项目的瞬时更改和更改。使用调度程序优先级 Normal 甚至 Send (最高优先级)抢占更改的显示。这就是我最后所做的。 See here for details about the DispatcherPriority enumeration.

        3
  •  5
  •   David Veeneman    14 年前

    知道了!我将接受马约卡的回答,因为他在回答下面的评论引导我找到了解决办法。

    这是我做的:我创造了一个 SelectionChanged 代码隐藏中的列表框的事件处理程序。是的,它很难看,但它起作用了。后面的代码还包含一个模块级变量, m_OldSelectedIndex ,初始化为-1。这个 选择已更改 处理程序调用ViewModel的 Validate() 方法,并返回一个布尔值,指示文档是否有效。如果文档有效,则处理程序将设置 已选定索引 到目前为止 ListBox.SelectedIndex 然后退出。如果文档无效,则处理程序将重置 列表框.selectedIndex 已选定索引 . 以下是事件处理程序的代码:

    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 属性。

    谢谢你的帮助,马约卡,希望这能帮助其他人。像我一样,六个月后,当我忘记了这个解决方案…

        4
  •  3
  •   g t Omri Btian    7 年前

    如果您认真对待MVVM,不希望有任何代码隐藏,也不喜欢使用 Dispatcher 坦白地说,它也不优雅,下面的解决方案对我来说是有效的,比这里提供的大多数解决方案都要优雅得多。

    它基于这样一个概念,即在代码后面,您可以使用 SelectionChanged 事件。现在,如果是这样的话,为什么不为它创建一个行为,并将命令与 选择已更改 事件。在视图模型中,您可以轻松记住以前选择的索引和当前选择的索引。诀窍是在 SelectedIndex 只要在选择发生变化的时候让它改变。但是在选择真正改变之后, 选择已更改 事件激发,现在通过命令通知您的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 .

        5
  •  1
  •   JLCNZ    10 年前

    最近我遇到了这个问题,并提出了一个解决方案,它可以很好地与我的MVVM配合使用,而不需要和代码隐藏。

    我在模型中创建了一个SelectedIndex属性,并将ListBox 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"); 
    

    自动取款机似乎工作得很好。可能会出现一些边缘情况,但现在,它正是我想要的。

        6
  •  0
  •   Gerard    11 年前

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

    如果不允许禁用的外观,样式可能会有所帮助,但由于不允许更改选择,禁用的样式可能是正常的。

    也许在.NET版本4.5中,inotifydata错误信息有帮助,我不知道。

        7
  •  0
  •   TCC    11 年前

    我有一个非常相似的问题,不同的是我正在使用 ListView 绑定到 ICollectionView 并且正在使用 IsSynchronizedWithCurrentItem 而不是约束 SelectedItem 的属性 列表视图 . 这对我很有效,直到我想取消 CurrentItemChanged 基础事件 图标集合视图 ,离开了 ListView.SelectedItem 与不同步 ICollectionView.CurrentItem

    这里的基本问题是保持视图与视图模型同步。显然,在视图模型中取消选择更改请求是很简单的。因此,就我而言,我们确实需要一个更具响应性的视图。我宁愿避免将Kludges放入我的视图模型中,以绕过 列表视图 同步。另一方面,我非常乐意在我的视图代码后面添加一些视图特定的逻辑。

    因此,我的解决方案是在代码隐藏中为listview选择连接自己的同步。就我而言,完全的MVVM比默认的 列表视图 具有 IsSynchronizedWithCurrentItem

    这是我的密码……这也允许从视图模型中更改当前项。如果用户单击列表视图并更改所选内容,它将立即更改,如果下游的某个内容取消了更改(这是我所希望的行为),它将更改回原来的状态。注意我有 IsSynchronizedWithCurrentItem 在上设置为false 列表视图 . 还要注意,我正在使用 async / 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类中 图标集合视图 命名 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 可以使用某种对话服务从用户那里得到一个密切的确认。

        8
  •  0
  •   bwing    5 年前

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

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