WPF DataBinding: отменено изменение свойства — поле со списком не выравнивается

У меня есть форма WPF с полем со списком и текстовым полем (оба привязаны к свойству объекта). Изменение поля со списком или текстового поля обновляет свойство объекта, а привязка данных активирует и обновляет пользовательский интерфейс. Проблема в том, что я реализовал способ отмены изменения, который работает, но портит обновление пользовательского интерфейса. Если я внесу изменение из поля со списком и отменю его, поле со списком не вернет выбранное значение обратно к тому, чем оно должно быть (связанным со значением объекта). Если я внесу изменение из текстового поля и отменю его, и текстовое поле, и поле со списком отобразят правильные данные, но затем фокус мгновенно перейдет на поле со списком (когда оно должно было остаться в текстовом поле, так как это последнее место, которое у меня было Это). Я не совсем уверен, как исправить это в общем аспекте, а не обрабатывать события изменений и проверять, что изменение не было отменено впоследствии (потому что тогда в чем смысл привязки данных?) ...

//User.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;

namespace MyTesting
{
    public class User : AbstractEntity
    {
        public User()
        {
            Rankings = new Dictionary<int,string>();

            Rankings.Add(1, "Newbie");
            Rankings.Add(10, "Novice");
            Rankings.Add(25, "Adept User");
            Rankings.Add(50, "Power User");
            Rankings.Add(100, "Admin God");
        }

        public Dictionary<Int32, String> Rankings { get; set; }

        private Int32 _rank;
        public Int32 Rank
        {
            get
            {
                return _rank;
            }
            set
            {
                SetProperty<Int32>("Rank", ref _rank, value);
            }
        }
    }
}


//AbstractEntity.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;

namespace MyTesting
{
    public abstract class AbstractEntity : INotifyPropertyChanging, INotifyPropertyChanged
    {
        protected void SetProperty<T>(String propertyName, ref T property, T value)
        {
            if (!Object.Equals(property, value))
            {
                if (OnPropertyChanging(propertyName, property, value))
                {
                    T oldValue = (T)property;
                    property = value;
                    OnPropertyChanged(propertyName, property, value);
                }
            }
        }

        [field: NonSerialized]
        public event PropertyChangingEventHandler PropertyChanging;

        protected virtual Boolean OnPropertyChanging(String propertyName, Object oldValue = null, Object newValue = null)
        {
            CancellablePropertyChangingEventArgs e;

            if ((oldValue != null) || (newValue != null))
                e = new CancellablePropertyChangingEventArgs(propertyName, oldValue, newValue);
            else
                e = new CancellablePropertyChangingEventArgs(propertyName);

            return OnPropertyChanging(e);
        }
        protected virtual Boolean OnPropertyChanging(CancellablePropertyChangingEventArgs e)
        {
            if (PropertyChanging != null)
                PropertyChanging(this, e as PropertyChangingEventArgs);

            return !e.IsCancelled;
        }

        [field: NonSerialized]
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(String propertyName, Object oldValue = null, Object newValue = null)
        {
            ExtendedPropertyChangedEventArgs e;

            if ((oldValue != null) || (newValue != null))
                e = new ExtendedPropertyChangedEventArgs(propertyName, oldValue, newValue);
            else
                e = new ExtendedPropertyChangedEventArgs(propertyName);

            OnPropertyChanged(e);
        }
        protected virtual void OnPropertyChanged(ExtendedPropertyChangedEventArgs e)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, e as PropertyChangedEventArgs);
        }
    }

    public class ExtendedPropertyChangedEventArgs : PropertyChangedEventArgs
    {
        public ExtendedPropertyChangedEventArgs(String propertyName)
            : base(propertyName)
        {
        }

        public ExtendedPropertyChangedEventArgs(String propertyName, Object oldValue, Object newValue)
            : base(propertyName)
        {
            OldValue = oldValue;
            NewValue = newValue;
        }

        public Object OldValue { get; private set; }
        public Object NewValue { get; private set; }
    }

    public class CancellablePropertyChangingEventArgs : PropertyChangingEventArgs
    {
        public CancellablePropertyChangingEventArgs(String propertyName, Boolean cancel = false)
            : base(propertyName)
        {
            IsCancelled = cancel;
        }

        public CancellablePropertyChangingEventArgs(String propertyName, Object oldValue, Object newValue, Boolean cancel = false)
            : base(propertyName)
        {
            OldValue = oldValue;
            NewValue = newValue;

            IsCancelled = cancel;
        }

        public Object OldValue { get; private set; }
        public Object NewValue { get; private set; }

        public Boolean IsCancelled { get; set; }
    }
}


<!-- MainWindow.xaml -->
<Window x:Class="ObservableDictionaryBinding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:src="clr-namespace:MyTesting"
        Title="MainWindow" Height="350" Width="525" Loaded="OnLoaded">

    <Grid>
        <ComboBox x:Name="RankList" Height="23" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="12,12,12,0" />

        <TextBlock Height="23" Width="40" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="13,100,0,0" Text="Rank:" />
        <TextBox x:Name="RankBox" Height="23" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="59,97,12,0" />
    </Grid>
</Window>

//MainWindow.xaml.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace MyTesting
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            MyUser = new User();

            InitializeComponent();

            MyUser.PropertyChanging += new PropertyChangingEventHandler(MyUser_PropertyChanging);
        }

        private User MyUser { get; set; }

        private Binding RankListBinding { get; set; }
        private Binding RankBinding { get; set; }
        private Binding RankListRankBinding { get; set; }

        private void OnLoaded(object sender, EventArgs e)
        {
            DataContext = MyUser;

            RankListBinding = new Binding("Rankings");
            RankListBinding.Source = MyUser;
            RankList.SetBinding(ComboBox.ItemsSourceProperty, RankListBinding);
            RankList.SelectedValuePath = "Key";
            RankList.DisplayMemberPath = "Value";

            RankBinding = new Binding("Rank");
            RankBinding.Source = MyUser;
            RankBox.SetBinding(TextBox.TextProperty, RankBinding);

            RankListRankBinding = new Binding("Rank");
            RankListRankBinding.Source = MyUser;
            RankList.SetBinding(ComboBox.SelectedValueProperty, RankListRankBinding);
        }

        private void MyUser_PropertyChanging(Object sender, PropertyChangingEventArgs e)
        {
            CancellablePropertyChangingEventArgs ea = e as CancellablePropertyChangingEventArgs;

            String text = String.Format("Would you like to change the property '{0}' from '{1}' to '{2}'?",
                    e.PropertyName,
                    (ea.OldValue == null) ? "<null>" : ea.OldValue.ToString(),
                    (ea.NewValue == null) ? "<null>" : ea.NewValue.ToString()
                    );

            MessageBoxResult result = MessageBox.Show(this, text, "Property Changed",
                MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes);

            if (result == MessageBoxResult.No)
                ea.IsCancelled = true;
        }
    }
}

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

protected void SetProperty<T>(String propertyName, ref T property, T value)
{
    if (!Object.Equals(property, value))
    {
        bool cancelled = OnPropertyChanging<T>(propertyName, property, value);

        if (cancelled)
        {
            Application.Current.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    OnPropertyChanged<T>(propertyName);
                }),
                DispatcherPriority.ContextIdle,
                null
            );

            return;
        }

        T originalValue = property;
        property = value;
        OnPropertyChanged(propertyName, originalValue, property);
    }
}

person myermian    schedule 19.07.2010    source источник


Ответы (2)


Это решает пользовательский интерфейс, отображающий правильные данные с привязкой к данным... это просто не устраняет проблему с украденным фокусом:

protected void SetProperty<T>(String propertyName, ref T property, T value)
{
    if (!Object.Equals(property, value))
    {
        bool cancelled = OnPropertyChanging<T>(propertyName, property, value);

        if (cancelled)
        {
            Application.Current.Dispatcher.BeginInvoke(
                new Action(() =>
                {
                    OnPropertyChanged<T>(propertyName);
                }),
                DispatcherPriority.ContextIdle,
                null
            );

            return;
        }

        T originalValue = property;
        property = value;
        OnPropertyChanged(propertyName, originalValue, property);
    }
}
person myermian    schedule 19.07.2010

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

person Wallstreet Programmer    schedule 19.07.2010
comment
Я попытался исправить это, установив свойство, а затем вернув ему старое значение, каждый раз вызывая OnPropertyChanged, что вызывает 2 отдельных события (как и ожидалось, с двумя разными вызовами), но в конце Combobox все еще не обновляется правильно при отмене. (О, и я также обновил дженерики для других методов, не то чтобы это на что-то повлияло). - person myermian; 19.07.2010