WPF:取消数据绑定列表框中的用户选择?
如何在数据绑定的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
属性绑定的视图模型属性同步?谢谢你的帮助。
-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()。
更优雅的解决方案是添加IsCurrentDocumentValid属性或只是一个Validate()方法在视图模型上并在视图中使用它来允许或禁止选择更改。 – majocha 2010-04-09 20:05:11
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个月的时候我已经忘记了这个解决方案...
对于未来在这个问题上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");
}
}
注:笔者采用ContextIdle
为DispatcherPriority
的行动来恢复原状。虽然很好,但它的优先级低于Render
,这意味着更改将显示在用户界面中,因为所选项目会瞬间改变并返回。使用调度员优先级Normal
或甚至Send
(最高优先级)抢先显示更改。这就是我最终做的。 See here for details about the DispatcherPriority
enumeration.
我是一个说不完的人,而这正是我所寻找的。我唯一要补充的是,你需要检查'Application.Current'是否为单元测试为null并且相应地处理。 – 2011-09-28 00:29:55
Right - 'Application.Current'在正常操作中永远不会为null,因为如果Application()没有被实例化,绑定引擎就不会调用setter,但是你用单元测试提出了一个很好的观点。 – Aphex 2011-09-28 14:36:56
Application.Current.Dispatcher可以为null ...对于某些类型的项目...请改为使用Dispatcher.CurrentDispatcher。 – 2014-01-08 13:55:22
绑定ListBox
的属性:IsEnabled="{Binding Path=Valid, Mode=OneWay}"
其中Valid
是与审定algoritm视图模型属性。其他解决方案在我眼中看起来太牵强了。
当不允许禁用外观时,样式可能会有帮助,但可能禁用的样式是可以的,因为不允许更改选择。
也许在.NET版本4.5 INotifyDataErrorInfo帮助,我不知道。
我有一个非常类似的问题,不同的是我使用ListView
绑定到一个ICollectionView
并使用IsSynchronizedWithCurrentItem
而不是绑定ListView
的SelectedItem
财产。这一直很好,直到我想取消ICollectionView
的CurrentItemChanged
事件,ListView.SelectedItem
与ICollectionView.CurrentItem
不同步。
这里的底层问题是保持视图与视图模型同步。显然,取消视图模型中的选择更改请求并不重要。因此,就我而言,我们确实需要更积极响应的观点。我宁愿避免在我的ViewModel中添加一些工具来解决同步的局限性。另一方面,我非常乐意为我的代码隐藏添加一些特定于视图的逻辑。
所以我的解决方案是为代码隐藏中的ListView选择连线我自己的同步。就我而言,完美的MVVM和比ListView
和IsSynchronizedWithCurrentItem
的默认值更强大。
这里是我的代码背后......这也允许从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
的实施可以使用某种形式的对话服务的引出来自用户的紧密确认。
最近我遇到了这个问题,并提出了一个与我的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。可能会出现边缘情况,但现在它完全符合我的要求。
如果您对遵循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
。
伟大的解决方案:) – Adassko 2016-03-31 14:32:34
唯一的解决方案,为我工作!不能够感谢你,我花更多的时间试图解决这个问题,而不是我应该有的。 – 2016-05-13 08:06:27
必须在Behavior类的Command.Execute上添加一个空值检查,否则就是很好的解决方案。非常感激。 :-) – 2017-01-20 21:57:46
'SearchResults'集合在创建控件后的任何时候是否更改?我认为在任何时候或者SelectedItem对象来自不同的集合时,ItemsSource必然会发生更改的集合可能存在问题。 – 2010-04-09 15:26:16
这是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