WPF: annuler une sélection d'utilisateur dans une ListBox de base de données?

Comment annuler une sélection d'utilisateur dans une ListBox WPF de base de données? La propriété source est définie correctement, mais la sélection ListBox est désynchronisée.

J'ai une application MVVM qui doit annuler une sélection d'utilisateur dans une ListBox WPF si certaines conditions de validation échouent. La Validation est déclenchée par une sélection dans la ListBox, plutôt que par un bouton Soumettre.

La propriété ListBox.SelectedItem est liée à une propriété ViewModel.CurrentDocument. Si la validation échoue, le setter de la propriété view model se termine sans modification de la propriété. Ainsi, la propriété à laquelle ListBox.SelectedItem est liée n'est pas modifiée.

Si cela se produit, le setter de propriété du modèle de vue déclenche L'événement PropertyChanged avant sa sortie, ce que j'avais supposé être suffisant pour réinitialiser la ListBox à l'ancienne sélection. Mais cela ne fonctionne pas-la ListBox affiche toujours la nouvelle sélection d'utilisateur. J'ai besoin de remplacer cette sélection et de la synchroniser avec la propriété source.

Juste au cas où ce n'est pas clair, voici un exemple: la ListBox comporte deux éléments, Document1 et Document2; Document1 est sélectionné. L'utilisateur sélectionne Document2, mais Document1 ne parvient pas à valider. La propriété ViewModel.CurrentDocument est toujours définie sur Document1, mais la ListBox indique que Document2 est sélectionné. J'ai besoin de récupérer la sélection ListBox à Document1.

Voici ma liaison ListBox:

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

J'ai essayé d'utiliser un rappel du ViewModel (en tant qu'événement) à la vue (qui s'abonne à l'événement), pour forcer la propriété SelectedItem à la vieille de sélection. Je passe l'ancien Document avec l'événement, et c'est le bon (l'ancienne sélection), mais la sélection de ListBox ne revient pas.

Alors, comment puis-je synchroniser la sélection ListBox avec la propriété view model à laquelle sa propriété SelectedItem est liée? Merci pour votre aide.

24
demandé sur David Veeneman 2010-04-09 18:01:38

7 réponses

-snip-

Eh bien oublier ce que j'ai écrit ci-dessus.

Je viens de faire une expérience, et en effet SelectedItem est désynchronisé chaque fois que vous faites quelque chose de plus chic dans le setter. Je suppose que vous devez attendre le retour du setter, puis modifier la propriété dans votre ViewModel de manière asynchrone.

Solution de travail rapide et sale (testée dans mon projet simple) en utilisant MVVM Light helpers: Dans votre setter, pour revenir à la valeur précédente de CurrentDocument

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

, Il fondamentalement, les files d'attente du changement de propriété sur le thread de L'interface utilisateur, la priorité ContextIdle s'assurera qu'il attendra que L'interface utilisateur soit dans un état cohérent. il semble que vous ne pouvez pas changer librement les propriétés de dépendance à l'intérieur des gestionnaires d'événements dans WPF.

Malheureusement, il crée un couplage entre votre modèle de vue et votre vue et c'est un hack laid.

Pour faire DispatcherHelper.Uidispatcher travail que vous devez faire DispatcherHelper.Initialize() en premier.

7
répondu majocha 2010-04-09 19:34:11

Pour les futurs trébucheurs sur cette question, cette page est ce qui a finalement fonctionné pour moi: http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx

C'est pour une combobox, mais fonctionne très bien pour une listbox, car dans MVVM, vous ne vous souciez pas vraiment du type de contrôle appelant le setter. Le secret glorieux, comme le mentionne l'auteur, est de en fait, changez la valeur sous-jacente, puis modifiez-la.{[14] } c'était aussi important d'exécuter ce "Annuler" sur une opération de répartiteur séparée.

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

Remarque:, L'auteur utilise ContextIdle de la DispatcherPriority pour l'action pour annuler la modification. Bien que cela soit correct, il s'agit d'une priorité inférieure à Render, ce qui signifie que la modification s'affichera dans l'interface utilisateur lorsque l'élément sélectionné changera momentanément et reviendra. L'utilisation d'une priorité de répartiteur de Normal ou même Send (la priorité la plus élevée) préempte l'affichage de la modification. C'est ce que j'ai fait. voir ici pour détails sur l'énumération DispatcherPriority.

33
répondu Aphex 2015-05-20 16:31:03

Obtenu! Je vais accepter la réponse de majocha, parce que son commentaire sous sa réponse m'a conduit à la solution.

Voici wnat que j'ai fait: j'ai créé un gestionnaire d'événements SelectionChanged pour la ListBox dans code-behind. Oui, c'est moche, mais ça fonctionne. Le code-behind contient également une variable de niveau module, m_OldSelectedIndex, qui est initialisée à -1. Le gestionnaire SelectionChanged appelle la méthode Validate() de ViewModel et obtient un retour booléen indiquant si le Document est valide. Si le Document est valide, le gestionnaire définit m_OldSelectedIndex sur ListBox.SelectedIndex courant et quitte. Si le document n'est pas valide, le gestionnaire réinitialise ListBox.SelectedIndex à m_OldSelectedIndex. Voici le code du gestionnaire d'événements:

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

Notez qu'il y a une astuce à cette solution: vous devez utiliser la propriété SelectedIndex; cela ne fonctionne pas avec la propriété SelectedItem.

Merci pour votre aide majocha, et j'espère que cela aidera quelqu'un d'autre sur la route. Comme moi, dans six mois, quand j'aurai oublié cette solution...

5
répondu David Veeneman 2010-04-09 22:53:49

Si vous êtes sérieux au sujet de suivre MVVM et ne voulez pas de code derrière, et n'aimez pas non plus l'utilisation du Dispatcher, qui n'est franchement pas élégant non plus, la solution suivante fonctionne pour moi et est de loin plus élégante que la plupart des solutions fournies ici.

Il est basé sur la notion que dans le code derrière vous êtes capable d'arrêter la sélection en utilisant l'événement SelectionChanged. Eh bien maintenant, si c'est le cas, pourquoi ne pas créer un comportement pour cela, et associer une commande à l'événement SelectionChanged. Dans le viewmodel vous pouvez ensuite facilement vous souvenir de l'index sélectionné précédent et de l'index sélectionné actuel. L'astuce consiste à avoir une liaison à votre viewmodel sur SelectedIndex et à laisser celui-ci changer chaque fois que la sélection change. Mais immédiatement après que la sélection a vraiment changé, l'événement SelectionChanged se déclenche qui est maintenant notifié via la commande à votre viewmodel. Comme vous vous souvenez de l'index sélectionné précédemment, vous pouvez le valider et, s'il n'est pas correct, vous déplacez l'index sélectionné vers l'index d'origine valeur.

Le code du comportement est le suivant:

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

L'utiliser 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>

Le code approprié dans viewmodel est le suivant:

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

Dans le constructeur du viewmodel:

SelectionChangedCommand = new RelayCommand(SelectionChanged);

RelayCommand fait partie de la lumière MVVM. Google si vous ne le savez pas. Vous devez vous référer à

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

Et donc vous devez référencer System.Windows.Interactivity.

3
répondu Philip Stuyck 2017-06-23 12:50:36

Je suis venu contre cela récemment, et est venu avec une solution qui fonctionne bien avec mon MVVM, sans avoir besoin de code et derrière.

J'ai créé une propriété SelectedIndex dans mon modèle et j'y ai lié la listbox SelectedIndex.

Sur L'événement View CurrentChanging, je fais ma validation, si elle échoue, j'utilise simplement le code

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

Il semble fonctionner parfaitement ATM. Il peut y avoir des cas de bord où cela ne marche pas, mais pour l'instant, il fait exactement ce que je veux.

1
répondu JLCNZ 2014-05-14 05:10:48

Bind la propriété de ListBox: IsEnabled="{Binding Path=Valid, Mode=OneWay}"Valid est la propriété view-model avec l'algorithme de validation. D'autres solutions semblent trop farfelues à mes yeux.

Lorsque l'apparence désactivée n'est pas autorisée, un style peut aider, mais probablement le style désactivé est ok car changer la sélection n'est pas autorisé.

Peut-être que dans la version. net 4.5 inotifydataerrorinfo aide, Je ne sais pas.

0
répondu Gerard 2013-11-04 15:07:48

J'ai eu un problème très similaire, la différence étant que j'utilise ListView lié à un ICollectionView et utilisait IsSynchronizedWithCurrentItem plutôt que de lier la propriété SelectedItem du ListView. Cela a bien fonctionné pour moi jusqu'à ce que je veuille annuler l'événement CurrentItemChanged du ICollectionView sous-jacent, ce qui a laissé le ListView.SelectedItem désynchronisé avec le ICollectionView.CurrentItem.

Le problème sous-jacent ici est de garder la vue synchronisée avec le modèle de vue. Évidemment, l'annulation d'une demande de changement de sélection dans le modèle de vue est triviale. Donc nous avons vraiment juste besoin d'une vue plus réactive en ce qui me concerne. Je préfère éviter de mettre kludges dans mon ViewModel pour contourner les limitations de la synchronisation ListView. D'un autre côté, je suis plus qu'heureux d'ajouter une logique spécifique à la vue à mon code de vue.

Donc, ma solution était de câbler ma propre synchronisation pour la sélection ListView dans le code-behind. Parfaitement MVVM en ce qui me concerne et plus robuste que la valeur par défaut pour ListView avec IsSynchronizedWithCurrentItem.

Voici mon code derrière ... cela permet également de modifier l'élément actuel à partir du ViewModel. Si l'utilisateur clique sur la vue liste et modifie la sélection, elle changera immédiatement, puis reviendra si quelque chose en aval annule le changement (c'est mon comportement souhaité). Notez que j'ai IsSynchronizedWithCurrentItem mis à false sur le ListView. Notez également que j'utilise async/await ici, qui joue bien, mais nécessite un peu de revérifier que lorsque le await revient, nous sommes toujours dans le même contexte de données.

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

Puis dans mon Classe ViewModel j'ai ICollectionView nommé Items, et cette méthode (une version simplifiée est présentée).

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

L'implémentation de {[21] } pourrait utiliser une sorte de service de dialogue pour obtenir une confirmation étroite de l'utilisateur.

0
répondu TCC 2013-12-10 22:44:21