开发者

How to use INotifyPropertyChanged correctly in WPF/XAML

I have a custom object that I am trying to bind to a control. On that custom object I have implemented the INotifyPropertyChanged interface. I have successfully bound to my object and a property on that object.

What I can't figure out is how to go from there. I've been working on this for 2 days now and I still cannot get it working.

My assumption was that when I would change the property bound to the control that the value set in that property would then show up in the control. However, no matter how much I change the property, the UI is never updated beyond it's initial value.

I have implemented INotifyPropertyChanged in this manner: A base class which implements INotifyPropertyChanged

So my base class is this:

[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
}

I inherit from that class above and in my derived class I do this on my property:

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

My XAML looks like this (abbreviated version):

<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>

You can see that the window's data context is a property called SizingProject. SizingProject is of the derived type (derived from BindableObject) and has the Name property in it and Raises the PropertyChanged event handler.

In my window's constructor I populate SizingProject and it's Name property is set.

To test this I also have a button on the window that fires an event that sets the Name property to something other than what it is originally. However when the name property is changed, nothing ever happens. I have traced back to the BindableObject and the PropertyChanged event is always set to null, so no handler is ever set and run. Why is this?

I thought by implementing INotifyPropertyChanged and using that type in a binding forces WPF to set that event handler automatically and then the correct behavior happens? For me, I never see that behavior.


I figured out the issue. What I needed to do was to create a DependencyProperty for my property SizingProject. After I did that, everything worked fine.

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

    public Sizing.Project Si开发者_如何学CzingProject
    {
        get
        {
            return (Sizing.Project)GetValue(Project.SizingProjectProperty);
        }
        set
        {
            SetValue(Project.SizingProjectProperty, value);
        }
    }


Works on my machine. Although, the cache is kind of wacky. I'd either create static readonly versions per type or forget about caching until required (premature optimization and all).

I created a sample project. The main window looks like this:

<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>

I'm binding to an instance of MyObject, which I create in the xaml (you can do this in the codebehind if this isn't a familiar thing to you).

Here's the code for 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");
        }
    }
}

How it all works together is:

  1. Window is created
  2. Instance of MyObject is instantiated and set to Window.DataContext
  3. TextBlock is bound to MyOutProperty
  4. TextBox is bound to MyInProperty
  5. User types 'X' in the Textbox
  6. MyInProperty is set with 'X'
  7. MyOutProperty is set with 'The textbox below says: "X"'
  8. MyOutProperty's Set method calls RaisePropertyChanged passing in "MyOutProperty"
  9. The TextBlock gets updated as expected.

I suspect your issue isn't with your base class, it is either with the implementation of your child classes OR in your bindings.

To help debug your bindings, follow the information at this link to configure visual studio for verbose binding trace output (ends up in the Output window or Immediate window, if you've configured that).

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜