Question

I am trying to bind a dependency property to an INotifyPropertyChanged-enabled property with a multi-level property path.

When the owner object of the property is not null, the binding works, but if the owner object is null, the binding does not do anything (the converter is not called and TargetNullValue is not used).

Here is some minimal sample code to reproduce the problem:


Window1.xaml.cs:

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.ComponentModel;

namespace NPCPropertyPath
{
    public abstract class VMBase : INotifyPropertyChanged
    {
        protected void OnPropertyChanged(string propertyName)
        {
            OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
        }

        protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
        {
            if (e == null) {
                throw new ArgumentNullException("e");
            }

            if (PropertyChanged != null) {
                PropertyChanged(this, e);
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

    public partial class Window1 : Window
    {
        private class MyConverter : IValueConverter
        {
            public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                if (value == null) {
                    return null;
                } else if (value is int) {
                    return (((int)value) + 15).ToString();
                } else {
                    return "no int";
                }
            }

            object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
            {
                throw new NotSupportedException();
            }
        }

        private class InnerVM : VMBase
        {
            private int myValue;

            public int MyValue {
                get {
                    return myValue;
                }
                set {
                    if (myValue != value) {
                        myValue = value;
                        OnPropertyChanged("MyValue");
                    }
                }
            }
        }

        private class OuterVM : VMBase
        {
            private InnerVM thing;

            public InnerVM Thing {
                get {
                    return thing;
                }
                set {
                    if (thing != value) {
                        thing = value;
                        OnPropertyChanged("Thing");
                    }
                }
            }
        }

        private readonly OuterVM vm = new OuterVM();

        public Window1()
        {
            InitializeComponent();

            var txt = new TextBlock();
            txt.SetBinding(TextBlock.TextProperty,
                           new Binding("Thing.MyValue") {
                            Source = vm,
                            Mode = BindingMode.OneWay,
                            Converter = new MyConverter(),
                            TargetNullValue = "(error)"
                           });
            container.Content = txt;

            var txt2 = new TextBlock();
            txt2.SetBinding(TextBlock.TextProperty,
                            new Binding("Thing") {
                                Source = vm,
                                Mode = BindingMode.OneWay,
                                Converter = new MyConverter(),
                                TargetNullValue = "(error)"
                            });
            container2.Content = txt2;
        }

        void Button_Click(object sender, RoutedEventArgs e)
        {
            vm.Thing = new InnerVM();
            vm.Thing.MyValue += 10;
        }
    }
}

Window1.xaml:

<Window x:Class="NPCPropertyPath.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="NPCPropertyPath" Height="300" Width="300">
    <StackPanel>
        <Button Content="Change value" Click="Button_Click"/>
        <ContentControl Name="container"/>
        <ContentControl Name="container2"/>
    </StackPanel>
</Window>

Of course, this is a significantly simplified form of my real application, where there is quite a bit more going on and the involved classes are not crammed together in two files (or even in the same assembly) where they can all see each other.

This sample displays a window with a button and two content controls. Each of the content controls contains a TextBlock whose Text property is bound to a property from a view-model. The view-model instance (of type OuterVM) is assigned to the Source property of each binding.

OuterVM implements INotifyPropertyChanged; it has a property Thing of type InnerVM (which also implements INotifyPropertyChanged), which in turn has a property MyValue.

The first text block is bound to Thing.MyValue, the second one just to Thing. Both of the bindings have a converter set, as well as a value for the TargetNullValue property that should be displayed on the text block if the target property is null.

The Thing property of the OuterVM instance is initially null. When clicking the button, something is assigned to that property.

The problem: Only the second text block displays anything initially. The one that is bound to Thing.MyValue neither invokes the converter (as evidenced by setting breakpoints), nor does it use the value of the TargetNullValue property.

Why? And how can I have the first text block display a default value instead of Thing.MyValue while Thing is not assigned?

Was it helpful?

Solution

For this purpose you should not use TargetNullValue, you should use FallbackValue property of the Binding.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top