views:

174

answers:

1

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 SizingProject
    {
        get
        {
            return (Sizing.Project)GetValue(Project.SizingProjectProperty);
        }
        set
        {
            SetValue(Project.SizingProjectProperty, value);
        }
    }
+1  A: 

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

Will
I am trying to Bind to a property an already instantiated object. The instantiated object is referenced in a property in the Window's code behind. I tried some of your syntax, but it complains in the DataContext part saying I'm binding to an object that doesn't have a constructor that takes no parameters. I don't want to bind to a new object. I am trying to bind to an already existing one.
Ristogod
Although I figured out the issue, I gave you credit anyways as you are the only one who tried to help. Thanks.
Ristogod
@rist so what was the issue?
Will
The issue ended up being that I had to expose my property being used in the DataContext with a corresponding DependencyProperty. After I did that, it worked just fine.Unfortunately however, I quickly ran into other issues with INotifyPropertyChanged. My objects are quite complex and there are many dependent properties throughout my custom objects and maintaining the PropertyChanged events were a maintenance nightmare. I ended up using this technique to solve all my issues.http://www.pochet.net/blog/2010/07/02/inotifypropertychanged-automatic-dependent-property-and-nested-object-support/
Ristogod