Отмена выбора в выпадающем списке в WPF с MVVM - PullRequest
25 голосов
/ 18 октября 2011

В моем приложении WPF есть поле со списком:

<ComboBox  ItemsSource="{Binding CompetitorBrands}" DisplayMemberPath="Value" 
   SelectedValuePath="Key" SelectedValue="{Binding Path=CompMfgBrandID, Mode=TwoWay,
   UpdateSourceTrigger=PropertyChanged}" Text="{Binding CompMFGText}"/>

Привязано к коллекции KeyValuePair<string, string>

Вот свойство CompMfgBrandID в моей ViewModel:

public string CompMfgBrandID
{
    get { return _compMFG; }
    set
    {    
        if (StockToExchange != null && StockToExchange.Where(x => !string.IsNullOrEmpty(x.EnteredPartNumber)).Count() > 0)
        {
            var dr = MessageBox.Show("Changing the competitor manufacturer will remove all entered parts from the transaction.  Proceed?",
                "Transaction Type", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
            if (dr != DialogResult.Yes)
                return;
        }

        _compMFG = value;
        StockToExchange.Clear();

        ...a bunch of other functions that don't get called when you click 'No'...
        OnPropertyChanged("CompMfgBrandID");
    }
}

Если вы выберете «да», он будет вести себя как положено.Элементы очищаются, а остальные функции вызываются.Если я выберу «Нет», он вернется и не очистит мой список или не вызовет какие-либо другие функции, что хорошо, но в поле со списком все еще отображается новый выбор.Мне нужно, чтобы вернуться к исходному выбору, как будто ничего не изменилось, когда пользователь выбирает «Нет».Как я могу сделать это?Я также попытался добавить e.Handled = true в коде позади, но безрезультатно.

Ответы [ 12 ]

18 голосов
/ 02 апреля 2012

Этого можно добиться универсальным и компактным способом, используя универсальное поведение Blend .

Поведение определяет свойство зависимости с именем SelectedItem, и вы должны поместить вашу привязку в это свойство, а не в свойство SelectedItem ComboBox. Поведение отвечает за передачу изменений в свойстве зависимости в ComboBox (или, в более общем случае, в селектор), и когда SelectedItem селектора изменяется, он пытается присвоить его собственному свойству SelectedItem. Если назначение завершается неудачно (возможно, из-за того, что связанный установщик свойств виртуальной машины отклонил назначение), поведение обновляет селектор SelectedItem текущим значением его свойства SelectedItem.

По разным причинам вы можете столкнуться со случаями, когда список элементов в Селекторе очищается, и выбранный элемент становится пустым (см. этот вопрос ). В этом случае вы обычно не хотите, чтобы ваше свойство VM становилось нулевым. Для этого я добавил свойство зависимости IgnoreNullSelection, которое по умолчанию имеет значение true. Это должно решить такую ​​проблему.

Это CancellableSelectionBehavior класс:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;

namespace MySampleApp
{
    internal class CancellableSelectionBehavior : Behavior<Selector>
    {
        protected override void OnAttached()
        {
            base.OnAttached();
            AssociatedObject.SelectionChanged += OnSelectionChanged;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();
            AssociatedObject.SelectionChanged -= OnSelectionChanged;
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(CancellableSelectionBehavior),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));

        public object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly DependencyProperty IgnoreNullSelectionProperty =
            DependencyProperty.Register("IgnoreNullSelection", typeof(bool), typeof(CancellableSelectionBehavior), new PropertyMetadata(true));

        /// <summary>
        /// Determines whether null selection (which usually occurs since the combobox is rebuilt or its list is refreshed) should be ignored.
        /// True by default.
        /// </summary>
        public bool IgnoreNullSelection
        {
            get { return (bool)GetValue(IgnoreNullSelectionProperty); }
            set { SetValue(IgnoreNullSelectionProperty, value); }
        }

        /// <summary>
        /// Called when the SelectedItem dependency property is changed.
        /// Updates the associated selector's SelectedItem with the new value.
        /// </summary>
        private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (CancellableSelectionBehavior)d;

            // OnSelectedItemChanged can be raised before AssociatedObject is assigned
            if (behavior.AssociatedObject == null)
            {
                System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke(new Action(() =>
                {
                    var selector = behavior.AssociatedObject;
                    selector.SelectedValue = e.NewValue;
                }));
            }
            else
            {
                var selector = behavior.AssociatedObject;
                selector.SelectedValue = e.NewValue;
            }
        }

        /// <summary>
        /// Called when the associated selector's selection is changed.
        /// Tries to assign it to the <see cref="SelectedItem"/> property.
        /// If it fails, updates the selector's with  <see cref="SelectedItem"/> property's current value.
        /// </summary>
        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (IgnoreNullSelection && (e.AddedItems == null || e.AddedItems.Count == 0)) return;
            SelectedItem = AssociatedObject.SelectedItem;
            if (SelectedItem != AssociatedObject.SelectedItem)
            {
                AssociatedObject.SelectedItem = SelectedItem;
            }
        }
    }
}

Это способ использовать его в XAML:

<Window x:Class="MySampleApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="My Smaple App" Height="350" Width="525"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:local="clr-namespace:MySampleApp"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        mc:Ignorable="d"
        d:DataContext="{d:DesignInstance local:MainWindowViewModel}">
    <StackPanel>
        <ComboBox ItemsSource="{Binding Options}">
            <i:Interaction.Behaviors>
                <local:CancellableSelectionBehavior SelectedItem="{Binding Selected}" />
            </i:Interaction.Behaviors>
        </ComboBox>
    </StackPanel>
</Window>

и это пример свойства VM:

private string _selected;

public string Selected
{
    get { return _selected; }
    set
    {
        if (IsValidForSelection(value))
        {
            _selected = value;
        }
    }
}
18 голосов
/ 03 февраля 2016

Очень простое решение для .NET 4.5.1 +:

<ComboBox SelectedItem="{Binding SelectedItem, Delay=10}" ItemsSource="{Binding Items}"  />

Это работает для меня в большинстве случаев. Вы можете откатить выделение в выпадающем списке, просто запустите NotifyPropertyChanged без присвоения значения.

17 голосов
/ 18 октября 2011

Для достижения этого под MVVM ....

1] Имейте прикрепленное поведение, которое обрабатывает событие SelectionChanged ComboBox. Это событие вызывается с некоторыми аргументами события, имеющими флаг Handled. Но установка его в true бесполезна для SelectedValue привязки. Привязка обновляет источник независимо от того, было ли обработано событие.

2] Следовательно, мы настраиваем привязку ComboBox.SelectedValue на TwoWay и Explicit.

3] Только когда ваш чек удовлетворен и в окне сообщения указано, что Yes - это когда мы выполняем BindingExpression.UpdateSource(). В противном случае мы просто вызываем BindingExpression.UpdateTarget(), чтобы вернуться к старому выбору.


В моем примере ниже у меня есть список KeyValuePair<int, int>, связанный с контекстом данных окна. ComboBox.SelectedValue связан с простым записываемым свойством MyKey Window.

XAML ...

    <ComboBox ItemsSource="{Binding}"
              DisplayMemberPath="Value"
              SelectedValuePath="Key"
              SelectedValue="{Binding MyKey,
                                      ElementName=MyDGSampleWindow,
                                      Mode=TwoWay,
                                      UpdateSourceTrigger=Explicit}"
              local:MyAttachedBehavior.ConfirmationValueBinding="True">
    </ComboBox>

Где MyDGSampleWindow - это x: Имя Window.

Код позади ...

public partial class Window1 : Window
{
    private List<KeyValuePair<int, int>> list1;

    public int MyKey
    {
        get; set;
    }

    public Window1()
    {
        InitializeComponent();

        list1 = new List<KeyValuePair<int, int>>();
        var random = new Random();
        for (int i = 0; i < 50; i++)
        {
            list1.Add(new KeyValuePair<int, int>(i, random.Next(300)));
        }

        this.DataContext = list1;
    }
 }

И прикрепленное поведение

public static class MyAttachedBehavior
{
    public static readonly DependencyProperty
        ConfirmationValueBindingProperty
            = DependencyProperty.RegisterAttached(
                "ConfirmationValueBinding",
                typeof(bool),
                typeof(MyAttachedBehavior),
                new PropertyMetadata(
                    false,
                    OnConfirmationValueBindingChanged));

    public static bool GetConfirmationValueBinding
        (DependencyObject depObj)
    {
        return (bool) depObj.GetValue(
                        ConfirmationValueBindingProperty);
    }

    public static void SetConfirmationValueBinding
        (DependencyObject depObj,
        bool value)
    {
        depObj.SetValue(
            ConfirmationValueBindingProperty,
            value);
    }

    private static void OnConfirmationValueBindingChanged
        (DependencyObject depObj,
        DependencyPropertyChangedEventArgs e)
    {
        var comboBox = depObj as ComboBox;
        if (comboBox != null && (bool)e.NewValue)
        {
            comboBox.Tag = false;
            comboBox.SelectionChanged -= ComboBox_SelectionChanged;
            comboBox.SelectionChanged += ComboBox_SelectionChanged;
        }
    }

    private static void ComboBox_SelectionChanged(
        object sender, SelectionChangedEventArgs e)
    {
        var comboBox = sender as ComboBox;
        if (comboBox != null && !(bool)comboBox.Tag)
        {
            var bndExp
                = comboBox.GetBindingExpression(
                    Selector.SelectedValueProperty);

            var currentItem
                = (KeyValuePair<int, int>) comboBox.SelectedItem;

            if (currentItem.Key >= 1 && currentItem.Key <= 4
                && bndExp != null)
            {
                var dr
                    = MessageBox.Show(
                        "Want to select a Key of between 1 and 4?",
                        "Please Confirm.",
                        MessageBoxButton.YesNo,
                        MessageBoxImage.Warning);
                if (dr == MessageBoxResult.Yes)
                {
                    bndExp.UpdateSource();
                }
                else
                {
                    comboBox.Tag = true;
                    bndExp.UpdateTarget();
                    comboBox.Tag = false;
                }
            }
        }
    }
}

В поведении я использую свойство ComboBox.Tag, чтобы временно сохранить флаг, который пропускает перепроверку, когда мы возвращаемся к старому выбранному значению.

Дайте мне знать, если это поможет.

6 голосов
/ 02 мая 2013

Я нашел гораздо более простой ответ на этот вопрос пользователя shaun в другой теме: https://stackoverflow.com/a/6445871/2340705

Основная проблема заключается в том, что событие измененного свойства проглатывается.Некоторые бы назвали это ошибкой.Чтобы обойти это, используйте BeginInvoke из Dispatcher для принудительного возврата события, измененного свойством, в конец очереди событий пользовательского интерфейса.Это не требует никаких изменений в xaml, никаких дополнительных классов поведения и одной строки кода, измененной для модели представления.

4 голосов
/ 18 октября 2011

Проблема в том, что, как только WPF обновляет значение с помощью установщика свойств, он игнорирует любые дальнейшие уведомления об изменениях свойств из этого вызова: он предполагает, что они будут происходить как обычная часть установщика и не будут иметь никакого значения, даже еслиВы действительно обновили свойство до первоначального значения.

Я обошел это так, что обновил поле, а также поставил в очередь действие в Диспетчере, чтобы "отменить" изменение.Действие вернуло бы его к старому значению и запустило уведомление об изменении свойства, чтобы WPF понял, что это на самом деле не новое значение, как он думал.

Очевидно, действие "отменить" должно быть настроено такв вашей программе не запускается бизнес-логика.

2 голосов
/ 14 июня 2013

У меня была та же самая проблема, причины потока UI и способ, которым работает связывание. Проверьте эту ссылку: SelectedItem на ComboBox

Структура в примере использует код позади, но MVVM точно такой же.

1 голос
/ 05 октября 2014

Вот общий поток, который я использую (не требует никаких поведений или модификаций XAML):

  1. Я просто позволил изменениям пройти через ViewModel и отследить все, что было передано ранее. (Если ваша бизнес-логика требует, чтобы выбранный элемент не находился в недопустимом состоянии, я предлагаю переместить его в сторону Модели). Этот подход также удобен для ListBox, которые визуализируются с использованием Radio Buttons, так как выполнение установки SelectedItem как можно скорее не помешает подсвечиванию переключателей при появлении окна сообщения.
  2. Я немедленно вызываю событие OnPropertyChanged независимо от значения, переданного в.
  3. Я помещаю любую логику отмены в обработчик и вызываю ее, используя SynchronizationContext.Post () (Кстати: SynchronizationContext.Post также работает для приложений Магазина Windows. Поэтому, если у вас есть общий доступ к коду ViewModel, этот подход все равно будет работать).

    public class ViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
    
        public List<string> Items { get; set; }
    
        private string _selectedItem;
        private string _previouslySelectedItem;
        public string SelectedItem
        {
            get
            {
                return _selectedItem;
            }
            set
            {
                _previouslySelectedItem = _selectedItem;
                _selectedItem = value;
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs("SelectedItem"));
                }
                SynchronizationContext.Current.Post(selectionChanged, null);
            }
        }
    
        private void selectionChanged(object state)
        {
            if (SelectedItem != Items[0])
            {
                MessageBox.Show("Cannot select that");
                SelectedItem = Items[0];
            }
        }
    
        public ViewModel()
        {
            Items = new List<string>();
            for (int i = 0; i < 10; ++i)
            {
                Items.Add(string.Format("Item {0}", i));
            }
        }
    }
    
1 голос
/ 16 мая 2012

Я предпочитаю пример кода "сплинтора", а не "AngelWPF".Их подходы довольно похожи, хотя.Я реализовал прикрепленное поведение CancellableSelectionBehavior, и оно работает так, как рекламируется.Возможно, это было просто, что код в примере сплинтора было легче подключить к моему приложению.Код в прикрепленном поведении AngelWPF содержал ссылки на тип KeyValuePair, который вызвал бы дополнительные изменения кода.

В моем приложении был ComboBox, в котором элементы, отображаемые в DataGrid, основаны на выбранном элементе.в ComboBox.Если пользователь внес изменения в DataGrid, а затем выбрал новый элемент в ComboBox, я бы предложил пользователю сохранить изменения с помощью кнопок Да | Нет | Отмена в качестве параметров.Если они нажали «Отмена», я хотел игнорировать их новый выбор в ComboBox и сохранить старый выбор.Это сработало как победитель!

Для тех, кто пугается, когда видит ссылки на Blend и System.Windows.Interactivity, вам не нужно устанавливать Microsoft Expression Blend.Вы можете загрузить Blend SDK для .NET 4 (или Silverlight).

Blend SDK для .NET 4

Blend SDK для Silverlight 4

О да, в моем XAML я фактически использую это как объявление пространства имен для Blend в этом примере:

xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
0 голосов
/ 21 июня 2019

- Xaml

 <ComboBox SelectedItem="{Binding SelectedItem, Mode=TwoWay, Delay=10}" ItemsSource="{Binding Items}"  />

- ViewModel

private object _SelectedItem;
public object SelectedItem 
{
    get { return _SelectedItem;}
    set {
           if(_SelectedItem == value)// avoid rechecking cause prompt msg
            { 
               return;
            } 
            MessageBoxResult result = MessageBox.Show
                    ("Continue change?", MessageBoxButton.YesNo);
            if (result == MessageBoxResult.No)
            {
                ComboBox combo = (ComboBox)sender;
                handleSelection = false;
                combo.SelectedItem = e.RemovedItems[0];
                return;
            }
            _SelectedItem = value;
            RaisePropertyChanged(); 
        }
}
0 голосов
/ 12 июня 2015

Я хотел бы завершить ответ сплинтора , потому что я наткнулся на проблему с отложенной инициализацией в OnSelectedItemChanged:

Когда OnSelectedItemChanged вызывается до назначения AssociatedObject, используя System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke может иметь нежелательные побочные эффекты, такие как попытка инициализировать newValue значением по умолчанию для выбора в выпадающем списке.

Таким образом, даже если ваша ViewModel обновлена, поведение вызовет изменение текущего значения ViewModel SelectedItem на выбор по умолчанию ComboBox, хранящегося в e.NewValue.Если ваш код вызывает диалоговое окно, пользователь будет предупрежден об изменении, хотя его нет.Я не могу объяснить, почему это происходит, возможно, проблема со временем.

Вот мое исправление

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Interactivity;

namespace MyApp
{
    internal class CancellableSelectionBehaviour : Behavior<Selector>
    {
        protected override void OnAttached()
        {
            base.OnAttached();

            if (MustPerfomInitialChange)
            {
                OnSelectedItemChanged(this, InitialChangeEvent);
                MustPerfomInitialChange = false;
            }

            AssociatedObject.SelectionChanged += OnSelectionChanged;
        }

        protected override void OnDetaching()
        {
            base.OnDetaching();

            AssociatedObject.SelectionChanged -= OnSelectionChanged;
        }

        public static readonly DependencyProperty SelectedItemProperty =
            DependencyProperty.Register("SelectedItem", typeof(object), typeof(CancellableSelectionBehaviour),
                new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnSelectedItemChanged));

        public object SelectedItem
        {
            get { return GetValue(SelectedItemProperty); }
            set { SetValue(SelectedItemProperty, value); }
        }

        public static readonly DependencyProperty IgnoreNullSelectionProperty =
            DependencyProperty.Register("IgnoreNullSelection", typeof(bool), typeof(CancellableSelectionBehaviour), new PropertyMetadata(true));

        /// <summary>
        /// Determines whether null selection (which usually occurs since the combobox is rebuilt or its list is refreshed) should be ignored.
        /// True by default.
        /// </summary>
        public bool IgnoreNullSelection
        {
            get { return (bool)GetValue(IgnoreNullSelectionProperty); }
            set { SetValue(IgnoreNullSelectionProperty, value); }
        }

        /// <summary>
        /// OnSelectedItemChanged can be raised before AssociatedObject is assigned so we must delay the initial change. 
        /// Using System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke has unwanted side effects.
        /// So we use this bool to know if OnSelectedItemChanged must be called afterwards, in OnAttached
        /// </summary>
        private bool MustPerfomInitialChange { get; set; }

        /// <summary>
        /// OnSelectedItemChanged can be raised before AssociatedObject is assigned so we must delay the initial change. 
        /// Using System.Windows.Threading.Dispatcher.CurrentDispatcher.BeginInvoke has unwanted side effects.
        /// So we use this DependencyPropertyChangedEventArgs to save the argument needed to call OnSelectedItemChanged.
        /// </summary>
        private DependencyPropertyChangedEventArgs InitialChangeEvent { get; set; }

        /// <summary>
        /// Called when the SelectedItem dependency property is changed.
        /// Updates the associated selector's SelectedItem with the new value.
        /// </summary>
        private static void OnSelectedItemChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var behavior = (CancellableSelectionBehaviour)d;

            // OnSelectedItemChanged can be raised before AssociatedObject is assigned so we must delay the initial change.
            if (behavior.AssociatedObject == null)
            {
                behavior.InitialChangeEvent = e;
                behavior.MustPerfomInitialChange = true;               
            }
            else
            {
                var selector = behavior.AssociatedObject;
                selector.SelectedValue = e.NewValue;               
            }
        }

        /// <summary>
        /// Called when the associated selector's selection is changed.
        /// Tries to assign it to the <see cref="SelectedItem"/> property.
        /// If it fails, updates the selector's with  <see cref="SelectedItem"/> property's current value.
        /// </summary>
        private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (IgnoreNullSelection && (e.AddedItems == null || e.AddedItems.Count == 0)) return;
            SelectedItem = AssociatedObject.SelectedItem;
            if (SelectedItem != AssociatedObject.SelectedItem)
            {
                AssociatedObject.SelectedItem = SelectedItem;
            }
        }
    }
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...