如何取消数据绑定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
属性绑定的视图模型属性同步返回?谢谢你的帮助。
答案 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");
}
}
注意:作者使用ContextIdle
为DispatcherPriority
撤消更改的操作。虽然很好,但优先级低于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}}事件,这导致ICollectionView
与ListView.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有帮助,我不知道。