Understanding WPF data binding and value converter interactions

GusP picture GusP · Jul 10, 2014 · Viewed 26.4k times · Source

I'm trying to understand what's actually happening behind the scenes on the simplified repro code below.

I have a single Window with a ListBox and a TextBlock that are bound together (i.e., master -> detail). I then have a ViewModel with a couple properties--a string and a date. For the date, I implemented a value converter (LongDateConverter).

I have a couple Debug.WriteLine() calls in the code that lead to the following output:

  • Launch app
    • In converter: ConverterProblem.MainWindowViewModel
    • In converter: null
  • Click on one of the two items in the list box
    • In converter: ConverterProblem.DataModel

The second and third calls to the IValueConverter method I think I understand. The second is null because the ListBox doesn't have a selected item yet. The third is for the item that I selected.

What I don't understand is:

  1. Why is the first call passed a value of type MainWindowViewModel?
  2. Why is that call even happening in the first place?

Here's my code:

MainWindow.xaml:

<Window x:Class="ConverterProblem.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:app="clr-namespace:ConverterProblem"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <app:LongDateConverter x:Key="longDateConverter"/>
    </Window.Resources>
    <StackPanel Orientation="Horizontal">
        <ListBox SelectedItem="{Binding Data}" ItemsSource="{Binding DataList}"
                 DisplayMemberPath="Name"/>
        <TextBlock Text="{Binding Converter={StaticResource longDateConverter}}" 
                   DataContext="{Binding Data}" />
    </StackPanel>
</Window>

MainWindow.xaml.cs:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace ConverterProblem 
{
    public class LongDateConverter : IValueConverter 
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture) 
        {
            if (value == null) {
                Debug.WriteLine("In converter: null");
                return "null";
            }

            Debug.WriteLine("In converter: " + value.GetType().ToString());

            if (value.GetType() == typeof(MainWindowViewModel))
                return "viewmodel";

            return ((DataModel)value).Date.ToLongDateString();
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            return null;
        }
    }

    public class DataModel
    {
        public string Name { get; set; }
        public DateTime Date { get; set; }
    }

    public class MainWindowViewModel : INotifyPropertyChanged
    {
        private DataModel _data;
        private List<DataModel> _dataList;

        public MainWindowViewModel()
        {
            _dataList = new List<DataModel> { 
                new DataModel { Date = DateTime.Now, Name = "John" }, 
                new DataModel { Date = DateTime.Now.AddDays(50), Name = "Sue" }
            };
        }

        public DataModel Data
        {
            get { return _data; }
            set
            {
                if (_data == value) return;

                _data = value;
                RaisePropertyChanged("Data");
            }
        }

        public List<DataModel> DataList
        {
            get { return _dataList; }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void RaisePropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null) {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public partial class MainWindow : Window
    {
        private MainWindowViewModel _viewModel;

        public MainWindow()
        {
            _viewModel = new MainWindowViewModel();
            DataContext = _viewModel;
            InitializeComponent();
        }
    }
}

Answer

Rohit Vats picture Rohit Vats · Jul 10, 2014

Issue is you have binded Text dependency prior of setting DataContext for TextBlock.

XAML files are compiled into BAML and on application run, it is loaded from BAML by XAMLLoader which parse XAML from top to bottom and set value for DP's accordingly.

Since, Text DP gets encountered first so it will try to first set it's value and DataContext is not set yet for TextBlock so it will inherit from its parent Window whose DataContext is set to MainWindowViewModel. Hence, you see MainWindowViewModel printed in your converter. And when DataContext is set all DP's binding will be re-evaluated as per new DataContext.


Replace your XAML to this and you will see MainWindowViewModel won't print any more:

<TextBlock DataContext="{Binding Data}"
           Text="{Binding Converter={StaticResource longDateConverter}}" />

Output:

In converter: null
In converter: ConverterProblem.DataModel