Подтвердить что ты не робот

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? Благодарим за помощь.

4b9b3361

Ответ 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 может использовать какой-то диалог, чтобы получить подтверждение от пользователя.