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

WPF:取消数据绑定列表框中的用户选择?,wpf,mvvm,wpf-controls,Wpf,Mvvm,Wpf Controls,如何取消数据绑定WPF列表框中的用户选择?源属性设置正确,但列表框选择不同步 我有一个MVVM应用程序,如果某些验证条件失败,它需要取消WPF列表框中的用户选择。验证由列表框中的选择触发,而不是由提交按钮触发 ListBox.SelectedItem属性绑定到ViewModel.CurrentDocument属性。如果验证失败,视图模型属性的setter将退出,而不更改属性。因此,ListBox.SelectedItem绑定到的属性不会更改 如果发生这种情况,视图模型属性设置器会在退出之前引发P

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

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

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

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

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

这是我的列表框绑定:

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

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

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

-snip-

忘了我上面写的吧

我刚刚做了一个实验,事实上,只要你在setter中做任何更有趣的事情,SelectedItem就会失去同步。我想您需要等待setter返回,然后在ViewModel中异步更改该属性

使用MVVM Light helpers的快速脏工作解决方案(在我的简单项目中测试): 在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()。

明白了!我将接受马约查的回答,因为他在回答下面的评论引导我找到了解决方案

下面是我所做的:我为代码隐藏中的列表框创建了一个
SelectionChanged
事件处理程序。是的,它很难看,但很管用。代码隐藏还包含一个模块级变量
m\u OldSelectedIndex
,该变量初始化为-1。
SelectionChanged
处理程序调用ViewModel的
Validate()
方法,并获取一个布尔值,指示文档是否有效。如果文档有效,处理程序将
m_OldSelectedIndex
设置为当前
列表框。SelectedIndex
并退出。如果文档无效,处理程序会将
列表框。SelectedIndex
重置为
m\u 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,希望这能帮助其他人。像我一样,六个月后,当我忘记了这个解决方案时…

对于这个问题上的未来绊脚石,这一页最终对我有效:

它适用于组合框,但也适用于列表框,因为在MVVM中,您并不真正关心调用setter的控件类型。正如作者所提到的,光荣的秘密是,实际上改变了潜在的价值,然后又改变回来。在单独的调度程序操作上运行此“撤消”也很重要。

private Person\u currentpersoncellable;
公众人士现任人员
{
得到
{
WriteLine(“Getting CurrentPersonCancellable”);
返回_currentPersonCellable;
}
设置
{
//存储当前值,以便
//如果需要的话,把它换回来。
var origValue=\u CurrentPersonCancelable;
//如果值没有更改,请不要执行任何操作。
如果(值==\u CurrentPersonCancelable)
返回;
//请注意,我们现在实际上更改了该值。
//这是必要的,因为WPF似乎要查询
//更改后的值。组合框
//喜欢知道值确实发生了变化。
_CurrentPersonCancellable=值;
如果(
MessageBox.Show(
“是否允许更改所选项目?”,
“继续”,
MessageBoxButton.YesNo
)!=MessageBoxResult。是
)
{
Debug.WriteLine(“选择已取消”);
//更改回该值,但请在
//UI已完成其当前上下文操作。

Application.Current.Dispatcher.BeginInvoke( 新操作(()=> { Debug.WriteLine( “Dispatcher BeginInvoke”+ “设置CurrentPersonCancellable。” ); //根据基础价值进行此操作
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;
}
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; 
}
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"); 
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);
    }
}
<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; }
SelectionChangedCommand = new RelayCommand(SelectionChanged);
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
<ListBox 
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, Delay=10}" />