wpf mvvm wpf-controls

WPF: ¿Cancelar una selección de usuario en un ListBox de datos?



mvvm wpf-controls (7)

¿Cómo cancelo una selección de usuario en un WPF ListBox con conexión de datos? La propiedad de origen está configurada correctamente, pero la selección de ListBox no está sincronizada.

Tengo una aplicación MVVM que necesita cancelar una selección de usuario en un ListBox de WPF si fallan ciertas condiciones de validación. La validación se desencadena por una selección en el ListBox, en lugar de por un botón Enviar.

La propiedad ListBox.SelectedItem está vinculada a una propiedad ViewModel.CurrentDocument . Si la validación falla, el colocador para la propiedad del modelo de vista sale sin cambiar la propiedad. Por lo tanto, la propiedad a la que se vincula ListBox.SelectedItem no se modifica.

Si eso sucede, el creador de propiedades del modelo de vista activa el evento PropertyChanged antes de que salga, lo que suponía que sería suficiente para restablecer el ListBox a la selección anterior. Pero eso no está funcionando: el ListBox todavía muestra la nueva selección de usuario. Necesito anular esa selección y volver a sincronizarla con la propiedad de origen.

En caso de que no esté claro, aquí hay un ejemplo: ListBox tiene dos elementos, Document1 y Document2; Documento1 está seleccionado. El usuario selecciona Document2, pero Document1 no puede validar. La propiedad ViewModel.CurrentDocument todavía se establece en Document1, pero el ListBox muestra que se ha seleccionado Document2. Necesito regresar la selección de ListBox a Document1.

Aquí está mi enlace ListBox:

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

Intenté usar una devolución de llamada desde ViewModel (como un evento) a la Vista (que se suscribe al evento), para forzar la propiedad SelectedItem a la selección anterior. Paso el documento anterior con el evento, y es el correcto (la selección anterior), pero la selección de ListBox no cambia.

Entonces, ¿cómo puedo volver a sincronizar la selección de ListBox con la propiedad del modelo de vista a la que está vinculada su propiedad SelectedItem ? Gracias por tu ayuda.


¡Lo tengo! Voy a aceptar la respuesta de Majocha, porque su comentario debajo de su respuesta me llevó a la solución.

Esto es lo que hice: Creé un controlador de eventos SelectionChanged para el ListBox en código subyacente. Sí, es feo, pero funciona. El código subyacente también contiene una variable a nivel de módulo, m_OldSelectedIndex , que se inicializa a -1. El controlador SelectionChanged llama al método Validate() ViewModel y obtiene un valor booleano que indica si el documento es válido. Si el documento es válido, el controlador establece m_OldSelectedIndex en el ListBox.SelectedIndex actual y sale. Si el documento no es válido, el controlador restablece ListBox.SelectedIndex a m_OldSelectedIndex . Aquí está el código para el controlador de eventos:

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

Tenga en cuenta que hay un truco para esta solución: debe usar la propiedad SelectedIndex ; no funciona con la propiedad SelectedItem .

Gracias por su ayuda majocha, y con suerte esto ayudará a alguien más en el futuro. Como yo, dentro de seis meses, cuando haya olvidado esta solución ...


-recorte-

Bueno, olvida lo que escribí arriba.

Acabo de hacer un experimento y, de hecho, SelectedItem no se sincroniza cada vez que haces algo más elegante en el setter. Supongo que debe esperar a que el colocador regrese y luego volver a cambiar la propiedad en su ViewModel de forma asincrónica.

Solución de trabajo rápida y sucia (probada en mi proyecto simple) usando ayudantes MVVM Light: en tu setter, para volver al valor anterior de CurrentDocument

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

básicamente pone en cola el cambio de propiedad en el subproceso de interfaz de usuario, la prioridad ContextIdle garantizará que espere a que la interfaz de usuario esté en estado constante. Parece que no puede cambiar libremente las propiedades de dependencia mientras está dentro de los manejadores de eventos en WPF.

Desafortunadamente, crea un acoplamiento entre su modelo de vista y su vista y es un hack feo.

Para hacer que DispatcherHelper.UIDispatcher funcione, primero debe hacer DispatcherHelper.Initialize ().


Me enfrenté a esto recientemente, y se me ocurrió una solución que funciona bien con mi MVVM, sin necesidad ni código.

Creé una propiedad SelectedIndex en mi modelo y afecté el cuadro de lista SelectedIndex a ella.

En el evento View CurrentChanging, hago mi validación, si falla, simplemente uso el código

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

Parece funcionar perfectamente en ATM. Puede haber casos límite donde no lo hace, pero por ahora, hace exactamente lo que yo quiero.


Para los futuros participantes en esta pregunta, esta página es lo que finalmente funcionó para mí: blog.alner.net/archive/2010/04/25/…

Es para un combobox, pero funciona bien para un listbox, ya que en MVVM realmente no te importa qué tipo de control está llamando al setter. El secreto glorioso, como lo menciona el autor, es cambiar realmente el valor subyacente y luego cambiarlo de nuevo. También era importante ejecutar este "deshacer" en una operación de despachador por separado.

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

Nota: El autor utiliza ContextIdle para DispatcherPriority para la acción para deshacer el cambio. Aunque está bien, esta es una prioridad más baja que Render , lo que significa que el cambio se mostrará en la UI como el elemento seleccionado cambiando y cambiando momentáneamente. El uso de una prioridad de despachador de Normal o incluso Send (la prioridad más alta) reemplaza la visualización del cambio. Esto es lo que terminé haciendo. Consulte aquí para obtener detalles sobre la enumeración DispatcherPriority .


Si realmente quiere seguir MVVM y no quiere ningún código, y tampoco le gusta el uso del Dispatcher , que francamente tampoco es elegante, la siguiente solución funciona para mí y es mucho más elegante que la mayoría de las soluciones proporcionadas aquí.

Se basa en la idea de que en el código que está detrás de usted puede detener la selección utilizando el evento SelectionChanged . Ahora bien, si este es el caso, ¿por qué no crear un comportamiento para él y asociar un comando con el evento SelectionChanged ? En el modelo de vista, puede recordar fácilmente el índice seleccionado previamente y el índice seleccionado actual. El truco es vincularlo a su modelo de vista en SelectedIndex y simplemente dejar que cambie cada vez que cambie la selección. Pero inmediatamente después de que la selección realmente haya cambiado, el evento SelectionChanged se dispara y ahora se notifica mediante su comando a su modelo de vista. Como recuerda el índice previamente seleccionado, puede validarlo y, si no es correcto, vuelve a mover el índice seleccionado al valor original.

El código para el comportamiento es el siguiente:

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

Utilizándolo en 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>

El código que es apropiado en el modelo de vista es el siguiente:

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

En el constructor del viewmodel:

SelectionChangedCommand = new RelayCommand(SelectionChanged);

RelayCommand es parte de la luz MVVM. Google si no lo sabes. Debes consultar

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

y por lo tanto, necesita hacer referencia a System.Windows.Interactivity .


Tuve un problema muy similar, la diferencia es que estoy usando ListView vinculado a un ICollectionView y estaba usando IsSynchronizedWithCurrentItem lugar de vincular la propiedad SelectedItem de ListView . Esto funcionó bien para mí hasta que quise cancelar el evento CurrentItemChanged del ICollectionView subyacente, que dejó el ListView.SelectedItem fuera de sincronización con el ICollectionView.CurrentItem .

El problema subyacente aquí es mantener la vista sincronizada con el modelo de vista. Obviamente, cancelar una solicitud de cambio de selección en el modelo de vista es trivial. Entonces realmente solo necesitamos una visión más receptiva en lo que a mí respecta. Prefiero evitar poner kludges en mi ViewModel para evitar las limitaciones de la sincronización ListView . Por otro lado, estoy más que feliz de agregar algo de lógica específica de vista a mi código de vista detrás.

Así que mi solución fue conectar mi propia sincronización para la selección ListView en el código subyacente. Perfectamente MVVM en lo que a mí respecta y más robusto que el predeterminado para ListView con IsSynchronizedWithCurrentItem .

Aquí está mi código detrás ... esto también permite cambiar el elemento actual del ViewModel. Si el usuario hace clic en la vista de lista y cambia la selección, cambiará inmediatamente, luego volverá a cambiar si algo más adelante cancela el cambio (este es mi comportamiento deseado). Tenga en cuenta que tengo IsSynchronizedWithCurrentItem establecido en falso en el ListView . También tenga en cuenta que estoy utilizando async / await aquí, que funciona bien, pero requiere una pequeña comprobación doble de que cuando await devolución, todavía estamos en el mismo contexto de datos.

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

Luego, en mi clase ViewModel tengo ICollectionView named Items y este método (se presenta una versión simplificada).

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

La implementación de TryCloseAsync podría usar algún tipo de servicio de diálogo para obtener una confirmación cercana del usuario.


IsEnabled="{Binding Path=Valid, Mode=OneWay}" la propiedad de ListBox : IsEnabled="{Binding Path=Valid, Mode=OneWay}" donde Valid es la propiedad de modelo de vista con el algoritmo de validación. Otras soluciones parecen demasiado exageradas en mis ojos.

Cuando no se permite la apariencia de desactivado, un estilo podría ayudar, pero probablemente el estilo de desactivación está bien porque no se permite cambiar la selección.

Tal vez en .NET versión 4.5 INotifyDataErrorInfo ayuda, no lo sé.