2010-04-09 17 views
23

Làm cách nào để hủy lựa chọn người dùng trong Hộp danh sách WPF dữ liệu? Thuộc tính nguồn được đặt chính xác, nhưng lựa chọn ListBox không đồng bộ.WPF: Hủy bỏ lựa chọn người dùng trong một ListBox databound?

Tôi có ứng dụng MVVM cần hủy lựa chọn người dùng trong Hộp danh sách WPF nếu các điều kiện xác thực nhất định không thành công. Xác nhận được kích hoạt bởi một lựa chọn trong ListBox, chứ không phải bởi một nút Submit.

Thuộc tính ListBox.SelectedItem được liên kết với thuộc tính ViewModel.CurrentDocument. Nếu xác thực không thành công, trình thiết lập cho thuộc tính mô hình khung nhìn ra mà không thay đổi thuộc tính. Vì vậy, thuộc tính mà ListBox.SelectedItem bị ràng buộc không bị thay đổi.

Nếu điều đó xảy ra, trình xem thuộc tính mô hình xem sẽ tăng sự kiện PropertyChanged trước khi thoát, mà tôi đã giả định sẽ đủ để đặt lại ListBox trở lại lựa chọn cũ. Nhưng điều đó không hoạt động - ListBox vẫn hiển thị lựa chọn người dùng mới. Tôi cần phải ghi đè lựa chọn đó và đưa nó trở lại đồng bộ với thuộc tính nguồn.

Chỉ trong trường hợp không rõ ràng, đây là một ví dụ: ListBox có hai mục, Document1 và Document2; Tài liệu 1 được chọn. Người dùng chọn Document2, nhưng Document1 không xác nhận hợp lệ. Thuộc tính ViewModel.CurrentDocument vẫn được đặt thành Document1, nhưng ListBox hiển thị rằng Document2 được chọn. Tôi cần để có được sự lựa chọn ListBox trở lại Document1.

Dưới đây là ListBox tôi Binding:

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

Tôi đã thử sử dụng một callback từ ViewModel (như là một sự kiện) để xem (mà đặt mua đến sự kiện này), để buộc các tài sản SelectedItem trở lại lựa chọn cũ. Tôi vượt qua các tài liệu cũ với sự kiện này, và nó là một trong những chính xác (lựa chọn cũ), nhưng lựa chọn ListBox không thay đổi trở lại.

Vì vậy, làm cách nào để có được lựa chọn ListBox được đồng bộ hóa với thuộc tính mô hình chế độ xem mà thuộc tính SelectedItem của nó bị ràng buộc? Cảm ơn bạn đã giúp đỡ.

+0

Bộ sưu tập 'SearchResults' có thay đổi bất kỳ lúc nào sau khi kiểm soát được tạo không? Tôi nghĩ rằng có thể có một vấn đề với bộ sưu tập mà các ItemsSource bị ràng buộc để thay đổi bất cứ lúc nào hoặc khi đối tượng SelectedItem đến từ một bộ sưu tập khác nhau. –

+0

Đây là bản sao của http://stackoverflow.com/questions/2608071/wpf-cancel-a-user-selection-in-a-databound-listbox có nhiều câu trả lời hơn, bao gồm câu trả lời liên kết tới http: // blog .alner.net/archive/2010/04/25/canceling-selection-change-in-a-bound-wpf-combo-box.aspx – splintor

Trả lời

7

-snip-

Hãy quên những gì tôi đã viết ở trên.

Tôi vừa làm một thử nghiệm, và thực sự SelectedItem không đồng bộ bất cứ khi nào bạn làm bất cứ điều gì lạ mắt hơn trong setter. Tôi đoán bạn cần phải chờ cho setter quay trở lại, và sau đó thay đổi thuộc tính trở lại trong ViewModel của bạn một cách không đồng bộ.

nhanh và giải pháp làm việc bẩn (thử nghiệm trong dự án đơn giản của tôi) sử dụng người giúp đỡ MVVM sáng: Trong setter của bạn, để trở lại giá trị trước đó của CurrentDocument

   var dp = DispatcherHelper.UIDispatcher; 
       if (dp != null) 
        dp.BeginInvoke(
        (new Action(() => { 
         currentDocument = previousDocument; 
         RaisePropertyChanged("CurrentDocument"); 
        })), DispatcherPriority.ContextIdle); 

nó về cơ bản hàng đợi sự thay đổi tài sản trên thread UI , Ưu tiên ContextIdle sẽ đảm bảo nó sẽ chờ giao diện người dùng ở trạng thái nhất quán. nó Xuất hiện bạn không thể tự do thay đổi các thuộc tính phụ thuộc trong khi bên trong xử lý sự kiện trong WPF.

Thật không may nó tạo ra sự ghép nối giữa mô hình chế độ xem của bạn và chế độ xem của bạn và đó là một bản hack xấu xí.

Để làm cho DispatcherHelper.UIDispatcher hoạt động, bạn cần phải thực hiện DispatcherHelper.Initialize() trước tiên.

+2

Giải pháp thanh lịch hơn sẽ là thêm thuộc tính IsCurrentDocumentValid hoặc chỉ là phương thức Validate() trên viewmodel và sử dụng nó trong dạng xem để cho phép hoặc không cho phép thay đổi lựa chọn. – majocha

5

OK! Tôi sẽ chấp nhận câu trả lời của Majocha, bởi vì bình luận của ông bên dưới câu trả lời của ông đã dẫn tôi đến giải pháp.

Đây là wnat tôi đã làm: Tôi đã tạo một trình xử lý sự kiện SelectionChanged cho ListBox ở chế độ mã hóa. Có, nó xấu xí, nhưng nó hoạt động. Mã phía sau cũng chứa biến cấp mô-đun, m_OldSelectedIndex, được khởi tạo thành -1. Trình xử lý SelectionChanged gọi phương thức Validate() của ViewModel và nhận lại boolean cho biết Tài liệu có hợp lệ hay không. Nếu Tài liệu hợp lệ, người xử lý đặt m_OldSelectedIndex cho số ListBox.SelectedIndex và các lần thoát hiện tại. Nếu tài liệu không hợp lệ, trình xử lý sẽ đặt lại ListBox.SelectedIndex thành m_OldSelectedIndex. Đây là mã cho trình xử lý sự kiện:

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; 
    } 
} 

Lưu ý rằng có một mẹo để giải quyết: Bạn phải sử dụng thuộc tính SelectedIndex; nó không hoạt động với thuộc tính SelectedItem.

Cảm ơn sự giúp đỡ của bạn, và hy vọng điều này sẽ giúp người khác xuống đường. Giống như tôi, sáu tháng kể từ bây giờ, khi tôi đã quên giải pháp này ...

30

Đối với Stumblers tương lai về vấn đề này, trang này là những gì cuối cùng làm việc cho tôi: http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx

Đó là một combobox, nhưng công trình cho một listbox tốt, vì trong MVVM bạn không thực sự quan tâm kiểu điều khiển nào gọi là setter. Bí mật vinh quang, như tác giả đề cập, là thực sự thay đổi giá trị cơ bản và sau đó thay đổi lại. Điều quan trọng là chạy “hoàn tác” này trên một hoạt động điều phối riêng biệt.

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"); 
    } 
} 

Lưu ý: Tác giả sử dụng ContextIdle cho DispatcherPriority cho hành động để hoàn tác các thay đổi. Mặc dù vậy, đây là mức ưu tiên thấp hơn Render, điều đó có nghĩa là thay đổi sẽ hiển thị trong giao diện người dùng dưới dạng mục đã chọn trong giây lát thay đổi và thay đổi trở lại. Sử dụng ưu tiên điều phối của Normal hoặc thậm chí Send (mức độ ưu tiên cao nhất) trước khi hiển thị thay đổi. Đây là những gì tôi đã làm. See here for details about the DispatcherPriority enumeration.

+4

Tôi là một kẻ nói xấu, và đây chính là điều tôi đang tìm kiếm. Chỉ có điều tôi muốn thêm là bạn sẽ cần phải kiểm tra nếu 'Application.Current' là null cho các bài kiểm tra đơn vị và xử lý cho phù hợp. –

+1

Right - 'Application.Current' sẽ không bao giờ rỗng trong hoạt động bình thường, bởi vì công cụ liên kết sẽ không gọi hàm setter nếu' Application() 'không được khởi tạo - nhưng bạn tăng điểm tốt với các bài kiểm tra đơn vị. – Aphex

+2

Application.Current.Dispatcher có thể là null ... cho một số loại dự án ... sử dụng thay vì Dispatcher.CurrentDispatcher. –

0

Bind ListBox Thuộc tính của: IsEnabled="{Binding Path=Valid, Mode=OneWay}" trong đó Valid là thuộc tính kiểu xem với thuật toán xác thực. Các giải pháp khác nhìn quá xa vời trong mắt tôi.

Khi không cho phép xuất hiện bị vô hiệu hóa, kiểu có thể giúp đỡ, nhưng có thể kiểu bị tắt là ok vì việc thay đổi lựa chọn không được phép.

Có thể trong phiên bản .NET 4.5 INotifyDataErrorInfo giúp, tôi không biết.

0

Tôi đã có một vấn đề rất giống nhau, sự khác biệt được rằng tôi đang sử dụng ListView ràng buộc để một ICollectionView và đã sử dụng IsSynchronizedWithCurrentItem chứ không phải ràng buộc SelectedItem tài sản của ListView. Điều này làm việc tốt cho tôi cho đến khi tôi muốn hủy sự kiện CurrentItemChanged của số ICollectionView bên dưới, để lại ListView.SelectedItem không đồng bộ với số ICollectionView.CurrentItem.

Vấn đề cơ bản ở đây là giữ cho chế độ xem được đồng bộ hóa với mô hình chế độ xem. Rõ ràng hủy bỏ một yêu cầu thay đổi lựa chọn trong mô hình xem là tầm thường. Vì vậy, chúng tôi thực sự chỉ cần một cái nhìn phản hồi nhanh hơn khi tôi quan tâm.Tôi muốn tránh đưa kludges vào ViewModel của tôi để làm việc xung quanh các hạn chế của việc đồng bộ ListView. Mặt khác, tôi rất vui khi thêm một số logic cụ thể cho chế độ xem vào mã xem của tôi.

Vì vậy, giải pháp của tôi là để dây đồng bộ hóa của riêng tôi cho việc lựa chọn ListView trong mã-đằng sau. Hoàn hảo MVVM theo như tôi quan tâm và mạnh mẽ hơn mặc định cho ListView với IsSynchronizedWithCurrentItem.

Đây là mã của tôi sau ... điều này cũng cho phép thay đổi mục hiện tại từ ViewModel. Nếu người dùng nhấp vào chế độ xem danh sách và thay đổi lựa chọn, nó sẽ ngay lập tức thay đổi, sau đó thay đổi lại nếu thứ gì đó xuống dòng hủy bỏ thay đổi (đây là hành vi mong muốn của tôi). Lưu ý tôi có IsSynchronizedWithCurrentItem được đặt thành false trên ListView. Cũng lưu ý rằng tôi đang sử dụng async/await ở đây có thể phát độc đáo, nhưng yêu cầu kiểm tra lại đôi chút rằng khi trả về await, chúng tôi vẫn ở trong cùng ngữ cảnh dữ liệu.

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; 
} 

Sau đó, trong lớp ViewModel của tôi, tôi có ICollectionView tên Items và phương pháp này (một phiên bản đơn giản được trình bày).

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; 
} 

Việc triển khai TryCloseAsync có thể sử dụng một số loại dịch vụ thoại để gợi ý xác nhận chặt chẽ từ người dùng.

1

Tôi đã đề cập đến điều này gần đây và đã đưa ra giải pháp hoạt động tốt với MVVM của tôi mà không cần và viết mã.

Tôi đã tạo thuộc tính SelectedIndex trong mô hình của mình và ràng buộc danh sách SelectedIndex với nó.

Trên sự kiện Xem CurrentChanging, tôi làm xác nhận của tôi, nếu nó không thành công, tôi chỉ đơn giản là sử dụng mã

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"); 

Có vẻ như để làm việc một cách hoàn hảo ATM. Có thể có trường hợp cạnh nó không, nhưng bây giờ, nó thực hiện chính xác những gì tôi muốn.

3

Nếu bạn nghiêm túc về việc theo dõi MVVM và không muốn bất kỳ mã nào phía sau, và cũng không thích sử dụng số Dispatcher, thực sự là không thanh lịch, giải pháp sau đây phù hợp với tôi và hơn thế nữa thanh lịch hơn hầu hết các giải pháp được cung cấp ở đây.

Nó được dựa trên khái niệm rằng trong mã phía sau bạn có thể dừng việc lựa chọn bằng cách sử dụng sự kiện SelectionChanged. Bây giờ, nếu đây là trường hợp, tại sao không tạo ra một hành vi cho nó, và kết hợp một lệnh với sự kiện SelectionChanged. Trong viewmodel, bạn có thể dễ dàng nhớ chỉ mục đã chọn trước đó và chỉ mục đã chọn hiện tại. Bí quyết là có ràng buộc với viewmodel của bạn trên SelectedIndex và chỉ cho phép một thay đổi bất cứ khi nào lựa chọn thay đổi. Nhưng ngay sau khi lựa chọn thực sự đã thay đổi, vụ cháy sự kiện SelectionChanged hiện đã được thông báo qua lệnh tới chế độ xem của bạn. Bởi vì bạn nhớ chỉ mục đã chọn trước đây, bạn có thể xác thực nó và nếu không chính xác, bạn di chuyển chỉ mục đã chọn trở về giá trị ban đầu.

Mã cho hành vi này là như sau:

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); 
    } 
} 

Sử dụng nó trong 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> 

Mã đó là thích hợp trong viewmodel như sau:

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; } 

Trong hàm tạo của viewmodel:

SelectionChangedCommand = new RelayCommand(SelectionChanged); 

RelayCommand là một phần của ánh sáng MVVM. Google nếu bạn không biết điều đó. Bạn cần phải tham khảo

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

và do đó bạn cần phải tham khảo System.Windows.Interactivity.

+0

Giải pháp tuyệt vời :) – Adassko

+0

Chỉ có giải pháp phù hợp với tôi! Không thể cảm ơn đủ, tôi đã dành waay lâu hơn để giải quyết vấn đề này hơn là tôi nên có. –

+0

Đã phải thêm một kiểm tra cho null trên Command.Execute trong lớp Hành vi nhưng nếu không thì giải pháp tuyệt vời. Nhiều đánh giá cao. :-) –

Các vấn đề liên quan