2010-04-09 54 views
23

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

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

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

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

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

这里是我的列表框绑定:

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

我曾尝试使用从视图模型的回调(作为事件)的视图(其中订阅事件),迫使SelectedItem属性回旧的选择。我用事件传递旧文档,它是正确的(旧选择),但列表框选择不会改回。

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

+0

'SearchResults'集合在创建控件后的任何时候是否更改?我认为在任何时候或者SelectedItem对象来自不同的集合时,ItemsSource必然会发生更改的集合可能存在问题。 – 2010-04-09 15:26:16

+0

这是http://stackoverflow.com/questions/2608071/wpf-cancel-a-user-selection-in-a-databound-listbox其中有更多的答案,包括链接到http://博客.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx – splintor 2011-07-12 16:46:51

回答

7

-snip-

那么忘记我上面写的。

我刚刚做了一个实验,事实上,只要你在setter中做了更多的事情,SelectedItem就会不同步。我想你需要等待setter返回,然后异步地将属性更改回ViewModel。

快速使用MVVM光佣工肮脏的工作解决方案(在我简单的项目测试): 在你的二传手,要恢复到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

更优雅的解决方案是添加IsCurrentDocumentValid属性或只是一个Validate()方法在视图模型上并在视图中使用它来允许或禁止选择更改。 – majocha 2010-04-09 20:05:11

5

Got it!我会接受majocha的回答,因为他的回答下他的评论让我找到了解决办法。

这是我做的:我在代码隐藏中为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,并希望这将帮助其他人在路上。像我一样,从现在起6个月的时候我已经忘记了这个解决方案...

30

对于未来在这个问题上stumblers,这个页面是最终为我工作: 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,这意味着更改将显示在用户界面中,因为所选项目会瞬间改变并返回。使用调度员优先级Normal或甚至Send(最高优先级)抢先显示更改。这就是我最终做的。 See here for details about the DispatcherPriority enumeration.

+4

我是一个说不完的人,而这正是我所寻找的。我唯一要补充的是,你需要检查'Application.Current'是否为单元测试为null并且相应地处理。 – 2011-09-28 00:29:55

+1

Right - 'Application.Current'在正常操作中永远不会为null,因为如果Application()没有被实例化,绑定引擎就不会调用setter,但是你用单元测试提出了一个很好的观点。 – Aphex 2011-09-28 14:36:56

+2

Application.Current.Dispatcher可以为null ...对于某些类型的项目...请改为使用Dispatcher.CurrentDispatcher。 – 2014-01-08 13:55:22

0

绑定ListBox的属性:IsEnabled="{Binding Path=Valid, Mode=OneWay}"其中Valid是与审定algoritm视图模型属性。其他解决方案在我眼中看起来太牵强了。

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

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

0

我有一个非常类似的问题,不同的是我使用ListView绑定到一个ICollectionView并使用IsSynchronizedWithCurrentItem而不是绑定ListViewSelectedItem财产。这一直很好,直到我想取消ICollectionViewCurrentItemChanged事件,ListView.SelectedItemICollectionView.CurrentItem不同步。

这里的底层问题是保持视图与视图模型同步。显然,取消视图模型中的选择更改请求并不重要。因此,就我而言,我们确实需要更积极响应的观点。我宁愿避免在我的ViewModel中添加一些工具来解决同步的局限性。另一方面,我非常乐意为我的代码隐藏添加一些特定于视图的逻辑。

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

这里是我的代码背后......这也允许从ViewModel中更改当前项目。如果用户单击列表视图并更改选择,它将立即更改,如果下游取消了更改(这是我期望的行为),则返回。注意我在ListView上将IsSynchronizedWithCurrentItem设置为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的实施可以使用某种形式的对话服务的引出来自用户的紧密确认。

1

最近我遇到了这个问题,并提出了一个与我的MVVM很好地协作的解决方案,无需编写代码。

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

在视图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。可能会出现边缘情况,但现在它完全符合我的要求。

3

如果您对遵循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; } 

在viewmodel的构造函数中:

SelectionChangedCommand = new RelayCommand(SelectionChanged); 

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

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

,因此你需要引用System.Windows.Interactivity

+0

伟大的解决方案:) – Adassko 2016-03-31 14:32:34

+0

唯一的解决方案,为我工作!不能够感谢你,我花更多的时间试图解决这个问题,而不是我应该有的。 – 2016-05-13 08:06:27

+0

必须在Behavior类的Command.Execute上添加一个空值检查,否则就是很好的解决方案。非常感激。 :-) – 2017-01-20 21:57:46