2010-04-09 11 views
23

¿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.WPF: ¿Cancelar una selección de usuario en un ListBox de datos?

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 enlaza ListBox.SelectedItem no se modifica.

Si eso ocurre, el creador de propiedad del modelo de vista activa el evento PropertyChanged antes de que salga, lo que asumí que sería suficiente para restablecer el ListBox de nuevo 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.

Por si acaso 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 aún está configurada en Documento1, pero el ListBox muestra que está seleccionado Documento2. Necesito regresar la selección de ListBox a Document1.

Aquí está mi ListBox Encuadernación:

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

lo hice tratar de usar una devolución de llamada desde el modelo de vista (como un evento) a la vista (que se suscribe al evento), para obligar a la propiedad SelectedItem de nuevo a la vieja selección. 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 hago para que la selección de ListBox vuelva a sincronizarse con la propiedad de modelo de vista a la que está vinculada su propiedad SelectedItem? Gracias por tu ayuda.

+0

¿La colección 'SearchResults' cambia en algún momento después de que se crea el control? Creo que puede haber un problema con la colección que ItemsSource está obligado a cambiar en cualquier momento o cuando el objeto SelectedItem proviene de una colección diferente. –

+0

Esto es un duplicado de http://stackoverflow.com/questions/2608071/wpf-cancel-a-user-selection-in-a-databound-listbox que tiene más respuestas, incluida la que se vincula a http: // blog .alner.net/archive/2010/04/25/canceling-selection-change-in-a-bound-wpf-combo-box.aspx – splintor

Respuesta

7

-snip-

Bueno olvidar lo que he escrito anteriormente.

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.

rápido y solución de trabajo sucio (probado en mi proyecto simple) usando ayudantes MVVM Light: En su colocador, para volver al valor anterior de CurrentDocument

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

que básicamente pone en cola el cambio de propiedad en el hilo de interfaz de usuario La prioridad de ContextIdle asegura que esperará a que la IU 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 realizar DispatcherHelper.Initialize().

+2

Una solución más elegante sería agregar una propiedad IsCurrentDocumentValid o simplemente un método Validate() en el modelo de vista y utilícelo en la vista para permitir o no permitir el cambio de selección. – majocha

5

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

He aquí 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 de nivel de módulo, m_OldSelectedIndex, que se inicializa en -1. El controlador SelectionChanged llama al método Validate() de ViewModel y obtiene un booleano que indica si el documento es válido. Si el documento es válido, el controlador establece m_OldSelectedIndex en el actual ListBox.SelectedIndex y sale. Si el documento no es válido, el controlador restablece ListBox.SelectedIndex a m_OldSelectedIndex. Aquí está el código del 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; 
    } 
} 

en cuenta que hay un truco para esta solución: Usted tiene que utilizar 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. Al igual que yo, dentro de seis meses, cuando me he olvidado de esta solución ...

30

Para futuras stumblers sobre esta cuestión, esta página es lo que finalmente funcionó para mí: http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx

Es para un cuadro combinado, pero las obras para un listbox bien, ya que en MVVM realmente no te importa qué tipo de control está llamando al setter. El secreto glorioso, como el autor menciona, es en realidad cambiar el valor subyacente y luego volver a cambiarlo. 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 utilizaContextIdle para la DispatcherPriority para la acción de 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 que cambia y cambia momentáneamente. El uso de una prioridad de operador de Normal o incluso Send (la prioridad más alta) prevalece sobre la visualización del cambio. Esto es lo que terminé haciendo. See here for details about the DispatcherPriority enumeration.

+4

Fui un gran creador, y esto es exactamente lo que estaba buscando. Lo único que agregaría es que tendrá que comprobar si 'Application.Current' es nulo para las pruebas unitarias y manejarlo en consecuencia. –

+1

Derecha - 'Application.Current' nunca sería nulo en el funcionamiento normal, porque el motor vinculante no llamaría al setter si' Application() 'no se instanciaba, pero usted plantea un buen punto con las pruebas unitarias. – Aphex

+2

Application.Current.Dispatcher puede ser nulo ... para algunos tipos de proyectos ... use en su lugar Dispatcher.CurrentDispatcher. –

0

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

Cuando no se permite la apariencia deshabilitada, un estilo podría ayudar, pero probablemente el estilo deshabilitado sea correcto porque no se permite cambiar la selección.

Quizás en .NET versión 4.5 INotifyDataErrorInfo ayude, no lo sé.

0

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

El problema subyacente 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 configurado como falso en el ListView. También tenga en cuenta que estoy usando async/await que funciona muy bien, pero requiere una pequeña comprobación doble que cuando el await regresa, 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 de modelo de vista que tienen ICollectionView nombrado 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 utilizar algún tipo de servicio de diálogo para obtener una confirmación de cierre por parte del usuario.

1

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 até el cuadro de lista SelectedIndex a ella.

En el evento Ver CurrentChanging, hago mis validación, si no funciona, 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 que funciona perfectamente ATM. Puede haber casos límite donde no lo hace, pero por ahora, hace exactamente lo que yo quiero.

3

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 noción de que en el código detrás de usted puede detener la selección mediante el evento SelectionChanged. 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 deje que cambie cada vez que cambie la selección. Pero inmediatamente después de que la selección realmente ha 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 como sigue:

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

Su uso 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 modelo de vista:

SelectionChangedCommand = new RelayCommand(SelectionChanged); 

RelayCommand parte de MVVM light. Google si no lo sabes. Es necesario hacer referencia a

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

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

+0

Gran solución :) – Adassko

+0

¡La única solución que funcionó para mí! No puedo agradecerle lo suficiente, pasé más tiempo tratando de resolver este problema de lo que debería. –

+0

Tuve que agregar un cheque para null en Command.Execute en la clase de comportamiento pero, por lo demás, fue una excelente solución. Muy apreciado. :-) –

Cuestiones relacionadas