WPF: Отмена выбора пользователя в привязке к данным ListBox?
Как отменить выбор пользователя в базе данных WPF ListBox? Свойство source установлено правильно, но выбор ListBox не синхронизирован.
У меня есть приложение MVVM, которое должно отменять выбор пользователя в ListBox WPF, если определенные условия проверки не срабатывают. Проверка активируется выбором в ListBox, а не кнопкой "Отправить".
Свойство ListBox.SelectedItem
привязано к свойству ViewModel.CurrentDocument
. Если проверка не удалась, средство настройки для свойства модели вида выходит из строя без изменения свойства. Таким образом, свойство, к которому привязано ListBox.SelectedItem
, не изменяется.
Если это произойдет, средство определения свойств модели представления вызывает событие PropertyChanged до его выхода, которое, как я предполагал, будет достаточным для reset ListBox для старого выбора. Но это не работает - ListBox по-прежнему показывает новый пользовательский выбор. Мне нужно переопределить этот выбор и вернуть его в синхронизацию с исходным свойством.
На всякий случай, что неясно, вот пример: ListBox имеет два элемента: Document1 и Document2; Выбран Document1. Пользователь выбирает Document2, но Document1 не может быть проверен. Свойству ViewModel.CurrentDocument
по-прежнему установлено значение Document1, но ListBox показывает, что выбран Document2. Мне нужно вернуть список ListBox в Document1.
Вот моя привязка ListBox:
<ListBox
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" />
Я попытался использовать обратный вызов из ViewModel (как событие) для представления (который подписывается на событие), чтобы вернуть свойство SelectedItem в старый выбор. Я передаю старый документ с событием, и он является правильным (старый выбор), но выбор ListBox не изменяется.
Итак, как мне получить выбор ListBox в синхронизации с свойством модели представления, которому привязано свойство SelectedItem
? Благодарим за помощь.
Ответы
Ответ 1
-snip -
Хорошо, что я написал выше.
Я только что сделал эксперимент, и действительно, SelectedItem выходит из строя, когда вы делаете что-то более интересное в сеттер. Я думаю, вам нужно подождать, пока сеттер вернется, а затем измените свойство обратно в вашей ViewModel асинхронно.
Быстрое и грязное рабочее решение (проверено в моем простом проекте) с помощью MVVM Light helpers:
В своем сеттере, чтобы вернуться к предыдущему значению CurrentDocument
var dp = DispatcherHelper.UIDispatcher;
if (dp != null)
dp.BeginInvoke(
(new Action(() => {
currentDocument = previousDocument;
RaisePropertyChanged("CurrentDocument");
})), DispatcherPriority.ContextIdle);
он в основном ставит в очередь изменение свойства в потоке пользовательского интерфейса, приоритет ContextIdle гарантирует, что он будет ожидать согласования состояния пользовательского интерфейса. Появляется, что вы не можете свободно изменять свойства зависимостей, а внутри обработчиков событий в WPF.
К сожалению, это создает связь между вашей моделью просмотра и вашим представлением, и это уродливое взлома.
Чтобы сделать работу DispatcherHelper.UIDispatcher, вам нужно сначала выполнить DispatcherHelper.Initialize().
Ответ 2
Для будущих споткнул на этот вопрос, эта страница в конечном итоге сработала для меня:
http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx
Это для combobox, но работает для списка только отлично, так как в MVVM вам все равно, какой тип управления вызывает setter. Славный секрет, как отмечает автор, заключается в фактическом изменении базового значения, а затем его изменении. Было также важно запустить эту "отмену" на отдельной диспетчерской операции.
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 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");
}
}
Примечание: Автор использует ContextIdle
для DispatcherPriority
для действия, чтобы отменить изменение. В то время как это нормально, это более низкий приоритет, чем Render
, что означает, что это изменение будет отображаться в пользовательском интерфейсе, так как выбранный элемент моментально меняется и изменяется. При использовании приоритета диспетчера Normal
или даже Send
(самый высокий приоритет) выдается предупреждение об изменении. Это то, что я закончил делать. Подробнее см. здесь перечисление DispatcherPriority
.
Ответ 3
Получил! Я собираюсь принять майоча, потому что его комментарий под его ответом привел меня к решению.
Вот что я сделал: я создал обработчик событий SelectionChanged
для ListBox в коде. Да, это уродливо, но это работает. Кодировка также содержит переменную уровня модуля, m_OldSelectedIndex
, которая инициализируется -1. Обработчик SelectionChanged
вызывает метод ViewModel Validate()
и получает логический ответ, указывающий, является ли Документ действительным. Если документ действителен, обработчик устанавливает m_OldSelectedIndex
в текущий ListBox.SelectedIndex
и завершает работу. Если документ недействителен, обработчик сбрасывает ListBox.SelectedIndex
на m_OldSelectedIndex
. Вот код для обработчика событий:
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;
}
}
Обратите внимание, что есть трюк для этого решения: вы должны использовать свойство SelectedIndex
; он не работает с свойством SelectedItem
.
Спасибо за вашу помощь майоча, и, надеюсь, это поможет кому-то еще по дороге. Как и я, через шесть месяцев, когда я забыл это решение...
Ответ 4
Если вы серьезно относитесь к следующему MVVM и не хотите, чтобы какой-либо код позади, а также не нравится использование Dispatcher
, что, откровенно говоря, тоже не изящно, следующее решение работает для меня и, безусловно, более элегантный, чем большинство предлагаемых здесь решений.
Он основан на понятии, что в коде позади вы можете остановить выбор, используя событие SelectionChanged
. Хорошо, если это так, почему бы не создать для него поведение и связать команду с событием SelectionChanged
. В viewmodel вы можете легко запомнить предыдущий выбранный индекс и текущий выбранный индекс. Трюк состоит в том, чтобы иметь привязку к вашей viewmodel на SelectedIndex
и просто позволять этому изменять каждый раз, когда изменяется выбор. Но сразу же после того, как выбор действительно изменился, срабатывает событие SelectionChanged
, которое теперь уведомляется с помощью команды в вашей модели просмотра. Поскольку вы помните ранее выбранный индекс, вы можете проверить его, а если не правильно, вы перемещаете выбранный индекс обратно к исходному значению.
Код для поведения выглядит следующим образом:
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);
}
}
Используя его в 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>
Код, который подходит в модели просмотра, выглядит следующим образом:
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; }
В конструкторе viewmodel:
SelectionChangedCommand = new RelayCommand(SelectionChanged);
RelayCommand
является частью света MVVM. Google, если вы этого не знаете.
Вам нужно обратиться к
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
и, следовательно, вам нужно ссылаться на System.Windows.Interactivity
.
Ответ 5
Недавно я столкнулся с этим и придумал решение, которое хорошо работает с моим MVVM, без необходимости и кода.
Я создал свойство SelectedIndex в своей модели и привязал к нему список SelectedIndex.
В представлении View CurrentChanging я выполняю свою проверку, если это не удается, я просто использую код
e.cancel = true;
//UserView is my ICollectionView that bound to the listbox, that is currently changing
SelectedIndex = UserView.CurrentPosition;
//Use whatever similar notification method you use
NotifyPropertyChanged("SelectedIndex");
Кажется, отлично работает ATM. Там могут быть случаи кросс, где это не так, но на данный момент он делает именно то, что я хочу.
Ответ 6
Bind ListBox
свойство: IsEnabled="{Binding Path=Valid, Mode=OneWay}"
где Valid
- свойство view-model с алгоритмом проверки. Другие решения выглядят слишком надуманными в моих глазах.
Когда отключенный вид не разрешен, стиль может помочь, но, возможно, отключенный стиль в порядке, поскольку изменение выбора не разрешено.
Может быть, в версии .NET 4.5. Помогает интрофитDataErrorInfo, я не знаю.
Ответ 7
У меня была очень похожая проблема, разница заключалась в том, что я использую ListView
привязан к ICollectionView
и использовал IsSynchronizedWithCurrentItem
вместо привязки к свойству SelectedItem
ListView
. Это работало хорошо для меня, пока я не захотел отменить событие CurrentItemChanged
лежащего в основе ICollectionView
, что оставило ListView.SelectedItem
не синхронизированным с ICollectionView.CurrentItem
.
Основная проблема здесь заключается в сохранении представления в синхронизации с моделью представления. Очевидно, что аннулирование запроса на изменение выбора в модели просмотра является тривиальным. Поэтому нам действительно нужно более отзывчивое мнение, насколько мне известно. Я бы предпочел избежать кладков в мой ViewModel, чтобы обойти ограничения синхронизации ListView
. С другой стороны, я более чем счастлив добавить некоторую логику, зависящую от вида, к моему коду кода.
Итак, мое решение состояло в том, чтобы связать мою собственную синхронизацию для выбора ListView в коде. Отлично MVVM, насколько мне известно, и более надежным, чем значение по умолчанию для ListView
с IsSynchronizedWithCurrentItem
.
Вот мой код позади... это позволяет также изменять текущий элемент из ViewModel. Если пользователь нажимает на представление списка и меняет выбор, он немедленно изменяется, а затем меняет его обратно, если что-то вниз-поток отменяет изменение (это мое желаемое поведение). Примечание. У меня IsSynchronizedWithCurrentItem
установлено значение false на ListView
. Также обратите внимание, что я использую async
/await
здесь, который играет красиво, но требует немного двойной проверки того, что при возврате await
мы все еще находимся в одном и том же контексте данных.
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;
}
Затем в моем классе ViewModel у меня есть ICollectionView
с именем Items
, и этот метод (представлен упрощенный вариант).
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;
}
Реализация TryCloseAsync
может использовать какой-то диалог, чтобы получить подтверждение от пользователя.