2010-04-09 81 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