Как правильно использовать INotifyPropertyChanged в WPF / XAML - PullRequest
2 голосов
/ 17 августа 2010

У меня есть пользовательский объект, который я пытаюсь привязать к элементу управления. В этом пользовательском объекте я реализовал интерфейс INotifyPropertyChanged. Я успешно связал свой объект и свойство этого объекта.

Что я не могу понять, так это как оттуда идти. Я работаю над этим уже 2 дня и до сих пор не могу заставить его работать.

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

Я реализовал INotifyPropertyChanged следующим образом: Базовый класс, который реализует INotifyPropertyChanged

Итак, мой базовый класс такой:

[Serializable]
public abstract class BindableObject : INotifyPropertyChanged
{
    #region Data

    private static readonly Dictionary<string, PropertyChangedEventArgs> eventArgCache;
    private const string ERROR_MSG = "{0} is not a public property of {1}";

    #endregion // Data

    #region Constructors

    static BindableObject()
    {
        eventArgCache = new Dictionary<string, PropertyChangedEventArgs>();
    }

    protected BindableObject()
    {
    }

    #endregion // Constructors

    #region Public Members

    /// <summary>
    /// Raised when a public property of this object is set.
    /// </summary>
    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;

    /// <summary>
    /// Returns an instance of PropertyChangedEventArgs for 
    /// the specified property name.
    /// </summary>
    /// <param name="propertyName">
    /// The name of the property to create event args for.
    /// </param>  
    public static PropertyChangedEventArgs
        GetPropertyChangedEventArgs(string propertyName)
    {
        if (String.IsNullOrEmpty(propertyName))
            throw new ArgumentException(
                "propertyName cannot be null or empty.");

        PropertyChangedEventArgs args;

        // Get the event args from the cache, creating them
        // and adding to the cache if necessary.
        lock (typeof(BindableObject))
        {
            bool isCached = eventArgCache.ContainsKey(propertyName);
            if (!isCached)
            {
                eventArgCache.Add(
                    propertyName,
                    new PropertyChangedEventArgs(propertyName));
            }

            args = eventArgCache[propertyName];
        }

        return args;
    }

    #endregion // Public Members

    #region Protected Members

    /// <summary>
    /// Derived classes can override this method to
    /// execute logic after a property is set. The 
    /// base implementation does nothing.
    /// </summary>
    /// <param name="propertyName">
    /// The property which was changed.
    /// </param>
    protected virtual void AfterPropertyChanged(string propertyName)
    {
    }

    /// <summary>
    /// Attempts to raise the PropertyChanged event, and 
    /// invokes the virtual AfterPropertyChanged method, 
    /// regardless of whether the event was raised or not.
    /// </summary>
    /// <param name="propertyName">
    /// The property which was changed.
    /// </param>
    protected void RaisePropertyChanged(string propertyName)
    {
        this.VerifyProperty(propertyName);

        PropertyChangedEventHandler handler = this.PropertyChanged;
        if (handler != null)
        {
            // Get the cached event args.
            PropertyChangedEventArgs args =
                GetPropertyChangedEventArgs(propertyName);

            // Raise the PropertyChanged event.
            handler(this, args);
        }

        this.AfterPropertyChanged(propertyName);
    }

    #endregion // Protected Members

    #region Private Helpers

    [Conditional("DEBUG")]
    private void VerifyProperty(string propertyName)
    {
        Type type = this.GetType();

        // Look for a public property with the specified name.
        PropertyInfo propInfo = type.GetProperty(propertyName);

        if (propInfo == null)
        {
            // The property could not be found,
            // so alert the developer of the problem.

            string msg = string.Format(
                ERROR_MSG,
                propertyName,
                type.FullName);

            Debug.Fail(msg);
        }
    }

    #endregion // Private Helpers
}

Я наследую от этого класса выше, и в своем производном классе я делаю это на своем свойстве:

    public virtual string Name
    {
        get
        {
            return m_strName;
        }
        set
        {
            m_strName = value;
            RaisePropertyChanged("Name");
        }
    }

Мой XAML выглядит следующим образом (сокращенная версия):

<Window x:Class="PSSPECApplication.Windows.Project"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:System="clr-namespace:System;assembly=mscorlib"
    DataContext="{Binding SizingProject, RelativeSource={RelativeSource Self}}">
        <StackPanel VerticalAlignment="Center">
            <TextBox Name="txtProjectName" Text="{Binding Name}" />
        </StackPanel>

Вы можете видеть, что контекст данных окна является свойством, называемым SizingProject. SizingProject имеет производный тип (производный от BindableObject), имеет свойство Name и вызывает обработчик события PropertyChanged.

В конструкторе моего окна я заполняю SizingProject, и его свойство Name установлено.

Чтобы проверить это, у меня также есть кнопка в окне, которая запускает событие, которое устанавливает свойство Name в нечто иное, чем оно было изначально. Однако, когда свойство name изменяется, ничего не происходит. Я проследил до BindableObject и событие PropertyChanged всегда имеет значение null, поэтому никакой обработчик никогда не устанавливается и не запускается. Почему это?

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


Я понял проблему. Что мне нужно было сделать, это создать DependencyProperty для моего свойства SizingProject. После того, как я это сделал, все заработало нормально.

        public static readonly DependencyProperty SizingProjectProperty =
        DependencyProperty.Register("SizingProject", typeof(Sizing.Project), typeof(Project), new UIPropertyMetadata());

    public Sizing.Project SizingProject
    {
        get
        {
            return (Sizing.Project)GetValue(Project.SizingProjectProperty);
        }
        set
        {
            SetValue(Project.SizingProjectProperty, value);
        }
    }

1 Ответ

3 голосов
/ 17 августа 2010

Работает на моей машине. Хотя кеш какой то дурацкий. Я бы либо создавал статические версии только для чтения для каждого типа, либо забывал бы о кэшировании до тех пор, пока это не потребуется (преждевременная оптимизация и все).

Я создал пример проекта. Главное окно выглядит так:

<Window x:Class="INPCTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:this="clr-namespace:INPCTest"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <this:MyObject />
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <TextBlock
            Text="{Binding MyOutProperty}" />
        <TextBox
            Grid.Row="1"
            Text="{Binding MyInProperty, UpdateSourceTrigger=PropertyChanged}" />
    </Grid>
</Window>

Я привязываюсь к экземпляру MyObject, который я создаю в xaml (вы можете сделать это в коде, если вам это не знакомо).

Вот код для MyObject:

class MyObject : BindableObject
{
    private string _in;
    private string _out;
    public string MyOutProperty
    {
        get { return _out; }
        set { _out = value; this.RaisePropertyChanged("MyOutProperty"); }
    }
    public string MyInProperty
    {
        get { return _in; }
        set
        {
            _in = value;
            MyOutProperty = "The textbox below says: \"" + value + "\"";
            this.RaisePropertyChanged("MyInProperty");
        }
    }
}

Как все это работает вместе:

  1. Окно создано
  2. Экземпляр MyObject создается и устанавливается на Window.DataContext
  3. TextBlock привязан к MyOutProperty
  4. TextBox привязан к MyInProperty
  5. Пользователь вводит 'X' в текстовом поле
  6. MyInProperty устанавливается с 'X'
  7. MyOutProperty устанавливается с помощью 'Текст ниже: "X"
  8. MyOutProperty Установить вызовы метода RaisePropertyChanged , передавая "MyOutProperty"
  9. TextBlock обновляется как положено.

Я подозреваю, что ваша проблема не в базовом классе, а в реализации дочерних классов или в привязках.

Чтобы помочь отладить ваши привязки, следуйте информации по этой ссылке , чтобы настроить Visual Studio для вывода подробной трассировки привязки (заканчивается в окне «Вывод» или «Немедленное окно», если вы его настроили).

...