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.
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.
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
.
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...
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
.
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.
Bind la propriété de ListBox
: IsEnabled="{Binding Path=Valid, Mode=OneWay}"
où 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.
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.