WPF: отменить выбор пользователя в ListBox с привязкой к данным? - PullRequest
24 голосов
/ 09 апреля 2010

Как отменить выбор пользователя в WPF ListBox с привязкой к данным? Свойство источника установлено правильно, но выбор ListBox не синхронизирован.

У меня есть приложение MVVM, которое должно отменить выбор пользователя в WPB ListBox, если определенные условия проверки не выполняются. Проверка запускается выбором в ListBox, а не кнопкой «Отправить».

Свойство ListBox.SelectedItem привязано к свойству ViewModel.CurrentDocument. Если проверка не пройдена, средство установки для свойства модели представления завершается без изменения свойства. Таким образом, свойство, с которым связан ListBox.SelectedItem, не изменяется.

Если это произойдет, то установщик свойства модели представления вызывает событие PropertyChanged до его выхода, что, как я предполагал, будет достаточно, чтобы вернуть ListBox к старому выбору. Но это не работает - ListBox по-прежнему показывает новый выбор пользователя. Мне нужно переопределить этот выбор и вернуть его синхронно со свойством источника.

На всякий случай, если неясно, вот пример: ListBox имеет два элемента, Document1 и Document2; Документ1 выбран. Пользователь выбирает 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 (как событие) в View (который подписывается на событие), чтобы принудительно вернуть свойство SelectedItem к старому выбору. Я передаю старый документ с событием, и он правильный (старый выбор), но выбор ListBox не изменяется обратно.

Итак, как мне вернуть выбор ListBox в синхронизацию со свойством модели представления, с которым связано его свойство SelectedItem? Спасибо за вашу помощь.

Ответы [ 8 ]

35 голосов
/ 26 сентября 2011

Для будущих участников этого вопроса эта страница в конечном итоге сработала для меня: http://blog.alner.net/archive/2010/04/25/cancelling-selection-change-in-a-bound-wpf-combo-box.aspx

Это для комбинированного списка, но отлично работает со списком, так как в MVVM вас не волнует, какой тип элемента управления вызывает сеттер. Славный секрет, как упоминает автор, состоит в том, чтобы фактически изменило базовое значение, а затем вернуло его обратно. Также было важно выполнить эту «отмену» в отдельной диспетчерской операции.

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

Примечание: Автор использует ContextIdle для DispatcherPriority для действия по отмене изменения. Хотя это нормально, это более низкий приоритет, чем Render, что означает, что изменение будет отображаться в пользовательском интерфейсе как выбранный элемент, мгновенно изменяющийся и возвращающийся назад. Использование приоритета диспетчера Normal или даже Send (самый высокий приоритет) препятствует отображению изменения. Это то, что я в итоге сделал. Подробнее о перечислении DispatcherPriority см. Здесь.

8 голосов
/ 09 апреля 2010

-snip-

Хорошо забудь, что я написал выше.

Я только что провел эксперимент, и, действительно, SelectedItem не синхронизируется всякий раз, когда вы делаете что-то более необычное в сеттере. Я полагаю, вам нужно подождать, пока установщик вернется, и затем асинхронно изменить свойство обратно в ViewModel.

Быстрое и грязное рабочее решение (проверено в моем простом проекте) с помощью помощников MVVM Light: В вашем установщике, чтобы вернуться к предыдущему значению CurrentDocument

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

это в основном ставит в очередь изменение свойства в потоке пользовательского интерфейса, приоритет ContextIdle гарантирует, что он будет ожидать, пока пользовательский интерфейс будет в согласованном состоянии. Оказывается, вы не можете свободно изменять свойства зависимостей, находясь внутри обработчиков событий в WPF.

К сожалению, это создает связь между вашей моделью представления и вашим представлением, и это отвратительный взлом.

Чтобы заставить работать DispatcherHelper.UIDispatcher, сначала нужно выполнить DispatcherHelper.Initialize ().

6 голосов
/ 10 апреля 2010

Понял! Я собираюсь принять ответ Майохи, потому что его комментарий под его ответом привел меня к решению.

Вот что я сделал: я создал обработчик событий 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 голосов
/ 16 февраля 2019

В .NET 4.5 они добавили поле задержки в привязку. Если вы установите задержку, он будет автоматически ожидать обновления, поэтому нет необходимости в Dispatcher во ViewModel. Это работает для проверки всех элементов Selector, таких как свойства SelectedItem ListBox и ComboBox. Задержка в миллисекундах.

<ListBox 
ItemsSource="{Binding Path=SearchResults, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
SelectedItem="{Binding Path=CurrentDocument, Mode=TwoWay, Delay=10}" />
3 голосов
/ 02 января 2016

Если вы серьезно относитесь к отслеживанию MVVM и не хотите иметь какой-либо код позади, а также не любите использовать Dispatcher, что, честно говоря, тоже не изящно, следующее решение мне подходит и безусловно более элегантный, чем большинство решений, представленных здесь.

Он основан на том, что в коде позади вы можете остановить выбор, используя событие SelectionChanged. Теперь, если это так, почему бы не создать для него поведение и не связать команду с событием SelectionChanged. В модели представления вы можете легко запомнить предыдущий выбранный индекс и текущий выбранный индекс. Хитрость заключается в том, чтобы привязать вашу модель представления на 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; }

В конструкторе модели представления:

SelectionChangedCommand = new RelayCommand(SelectionChanged);

RelayCommand является частью MVVM light. Google это, если вы не знаете это. Вы должны обратиться к

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

и, следовательно, вам нужно ссылаться на System.Windows.Interactivity.

1 голос
/ 14 мая 2014

Я недавно столкнулся с этим и нашел решение, которое хорошо работает с моим MVVM, без необходимости и кода.

Я создал свойство SelectedIndex в своей модели и привязал к нему список SelectedIndex.

В событии View CurrentChanging я провожу проверку, в случае неудачи просто использую код

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

Вроде отлично работает банкомат. Могут быть крайние случаи, когда это не так, но сейчас он делает именно то, что я хочу.

0 голосов
/ 11 декабря 2013

У меня была очень похожая проблема, с той разницей, что я использую ListView, связанный с ICollectionView, и использовал IsSynchronizedWithCurrentItem вместо привязки свойства SelectedItem ListView. Это работало хорошо для меня, пока я не хотел отменить событие CurrentItemChanged базового ICollectionView, которое не синхронизировало ListView.SelectedItem с ICollectionView.CurrentItem.

Основной проблемой здесь является поддержание синхронизации представления с моделью представления. Очевидно, что отмена запроса на изменение выбора в модели представления является тривиальной. Так что нам действительно нужно более отзывчивое мнение, насколько я понимаю. Я бы предпочел не помещать клуджесы в мою ViewModel, чтобы обойти ограничения синхронизации ListView. С другой стороны, я более чем счастлив добавить некоторую логику, специфичную для представления, в мой код представления позади.

Таким образом, мое решение заключалось в том, чтобы подключить мою собственную синхронизацию для выбора ListView в коде позади. На мой взгляд, идеально MVVM и более надежный, чем стандартный для ListView с IsSynchronizedWithCurrentItem.

Вот мой код позади ... это позволяет также изменить текущий элемент из ViewModel. Если пользователь щелкает представление списка и изменяет выбор, он немедленно изменится, а затем вернется обратно, если что-то в нисходящем направлении отменяет изменение (это мое желаемое поведение). Обратите внимание, что для ListView установлено значение IsSynchronizedWithCurrentItem. Также обратите внимание, что я использую 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 могла бы использовать какую-то службу диалога, чтобы вызвать подтверждение от пользователя.

0 голосов
/ 04 ноября 2013

Bind ListBox свойство: IsEnabled="{Binding Path=Valid, Mode=OneWay}", где Valid - это свойство модели представления с алгоритмом проверки. Другие решения выглядят слишком надуманными в моих глазах.

Когда запрещенный внешний вид не разрешен, стиль может помочь, но, вероятно, отключенный стиль в порядке, потому что изменение выделения не разрешено.

Может быть, в .NET версии 4.5 INotifyDataErrorInfo помогает, я не знаю.

...