User control dependencyproperychanged event firing, but bound viewmodel not updating... What's going on?

StackOverflow https://stackoverflow.com/questions/21337269

Question

I recently stumbled across this issue with WPFs DatePicker control. Basically, the popup causes an exception when you:

  1. Set the DatePickers Control Template via a style
  2. Bind the IsEnabled to a property on your ViewModel
  3. Run your program and Enable the DatePicker
  4. Drop down the calendar popup
  5. disable the DatePicker, then re-enable it
  6. Drop down the calendar popup again

I know that's a very specific scenario, but it happens all the time in my application. An entire user control full of controls starts off as disabled, the user selects a record, (filling the form), edits, saves, repeats. The second time they run it, selecting a calendar from a DatePicker crashes it.

I've set the DatePicker's control template in a style to show a disabled text box when disabled, to better fit with the look of the rest of the disabled appearances (I did the same with comboboxes with no issue).

So, knowing that this is the cause of my issue, I set out to create my own user control to handle the disabled appearance of a DatePicker. It simply wraps a Datepicker, provides some dependency properties to bind to the DatePicker properties I care about, and swaps the template of the user control with a text box when disabled. That way I get the look I want, without having to trip over the DatePicker's bug.

My issue is that, while the DependencyPropertyChanged events are firing with new values when I select a new date on my user control, these values never make their way to the external viewmodel that I'm binding to. I've looked at it every which way, and it's not making any sense to me:

DisableableDatePicker Code Behind

public partial class DisableableDatePicker : UserControl
{
    public static readonly DependencyProperty SelectedDateProperty = DependencyProperty.Register("SelectedDate", typeof(DateTime?), typeof(DisableableDatePicker), new PropertyMetadata(DateChanged));

    private static void DateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        Console.WriteLine(((DateTime)e.NewValue).ToString("yyyy-MM-dd"));
    }

    public DateTime? SelectedDate
    {
        get { return (DateTime?)GetValue(SelectedDateProperty); }
        set { SetValue(SelectedDateProperty, value); }
    }

    public static readonly DependencyProperty DisplayDateEndProperty = DependencyProperty.Register("DisplayDateEnd", typeof(DateTime?), typeof(DisableableDatePicker));
    public DateTime? DisplayDateEnd
    {
        get { return (DateTime?)GetValue(DisplayDateEndProperty); }
        set { SetValue(DisplayDateEndProperty, value); }
    }

    public static readonly DependencyProperty DisplayDateStartProperty = DependencyProperty.Register("DisplayDateStart", typeof(DateTime?), typeof(DisableableDatePicker));
    public DateTime? DisplayDateStart
    {
        get { return (DateTime?)GetValue(DisplayDateStartProperty); }
        set { SetValue(DisplayDateStartProperty, value); }
    }

    public static readonly DependencyProperty BlackoutDatesProperty = DependencyProperty.Register("BlackoutDates", typeof(List<DateTime>), typeof(DisableableDatePicker));
    public List<DateTime> BlackoutDates
    {
        get { return (List<DateTime>)GetValue(BlackoutDatesProperty); }
        set { SetValue(BlackoutDatesProperty, value); }
    }

    public DisableableDatePicker()
    {
        InitializeComponent();

        ClearValue(SelectedDateProperty);
        ClearValue(DisplayDateEndProperty);
        ClearValue(DisplayDateStartProperty);
        ClearValue(BlackoutDatesProperty);
        ClearValue(IsEnabledProperty);
    }
}

I added in the DependencyPropertyChanged event 'DateChanged' to print the new value to the console whenever one is selected. These new values show in the console just fine, without any issue.

DisableableDatePicker XAML

<UserControl x:Class="ALERT_Human_Resources.View.DisableableDatePicker"
             ...             
             Margin="0,0,0,3" x:Name="root" >
    <UserControl.Resources>
        <local:InverseBooleanConverter x:Key="DisableableDatePicker_InvertBoolean" />
        <system:String x:Key="DateFormat">yyyy-MM-dd</system:String>
    </UserControl.Resources>
    <UserControl.Style>
        <Style TargetType="{x:Type UserControl}">
            <Style.Triggers>
                <Trigger Property="IsEnabled" Value="False">
                    <Setter Property="Template">
                        <Setter.Value>
                            <ControlTemplate>
                                <TextBox IsEnabled="False" Text="{Binding Path=SelectedDate, ElementName=root" />
                            </ControlTemplate>
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
    </UserControl.Style>
    <DatePicker
                    local:DependencyPropertyHost.BlackoutDates="{Binding Path=BlackoutDates, ElementName=root}"
                    DisplayDateEnd="{Binding Path=DisplayDateEnd, ElementName=root}" 
                    DisplayDateStart="{Binding Path=DisplayDateStart, ElementName=root}"
                    SelectedDate="{Binding Path=SelectedDate, ElementName=root, StringFormat={StaticResource DateFormat}}"
                    IsEnabled="{Binding Path=IsEnabled, ElementName=root}" />
</UserControl>

DisableableDatePicker Usage

<view:DisableableDatePicker Margin="0,0,0,3" 
    SelectedDate="{Binding HighlightDate, UpdateSourceTrigger=PropertyChanged}" 
    DisplayDateStart="{Binding StartDate}" 
    DisplayDateEnd="{Binding EndDate}" 
    IsEnabled="{Binding IsEditing}" 
    BlackoutDates="{Binding BlackoutDates}" />

DisableableDatePicker Bound ViewModel Property

public DateTime HighlightDate
{
    get
    {
        return SelectedDailyHighlight == null ? DateTime.MinValue : SelectedDailyHighlight.HighlightDate;
    }
    set
    {
        if (SelectedDailyHighlight != null && value != null && SelectedDailyHighlight.HighlightDate == value)
            return;
        SelectedDailyHighlight.HighlightDate = value;
        OnPropertyChanged("HighlightDate");
    }
}

So, when I put breakpoints in the ViewModels getter and setter, the getter is hit properly, but the setter is never called. I find this odd, because my dependency property is reporting that it sees the new values.

The final piece to this puzzle is how I bind the Blackout dates. I wanted to be able to bind to a simple list of dates. To do this easily, I've added the following at an attached property:

 public static readonly DependencyProperty BlackoutDatesProperty =
    DependencyProperty.RegisterAttached("BlackoutDates", typeof(List<DateTime>),
    typeof(DependencyPropertyHost), 
    new FrameworkPropertyMetadata(null, OnBlackoutDatesChanged));

public static List<DateTime> GetBlackoutDates(DependencyObject d)
{
    return (List<DateTime>)d.GetValue(BlackoutDatesProperty);
}

public static void SetBlackoutDates(DependencyObject d, List<DateTime> value)
{
    d.SetValue(BlackoutDatesProperty, value);
}

private static void OnBlackoutDatesChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    DatePicker datePicker = d as DatePicker;
    if (e.NewValue != null && datePicker != null)
    {
        List<DateTime> blackoutDates = (List<DateTime>)e.NewValue;
        var toRemove = datePicker.BlackoutDates.Select
                                                (x => x.Start).Except
                                                (blackoutDates.Select(y => y)).ToList();
        foreach (DateTime date in toRemove)
        {
            datePicker.BlackoutDates.Remove(datePicker.BlackoutDates.Single(x => x.Start == date));
        }
        foreach (DateTime date in blackoutDates)
        {
            if (!datePicker.BlackoutDates.Contains(date) && 
                date >= datePicker.DisplayDateStart && 
                date <= datePicker.DisplayDateEnd && 
                datePicker.SelectedDate != date)
            {
                datePicker.BlackoutDates.Add(new CalendarDateRange(date));
            }
        }
    }
}
Was it helpful?

Solution

By default, Dependency Properties bind OneWay. Meaning it will read from the View Model, but won't write to it. You have 2 options to fix this.

Option 1: In your binding set the mode to TwoWay.

<view:DisableableDatePicker Margin="0,0,0,3" 
    SelectedDate="{Binding HighlightDate}" 
    DisplayDateStart="{Binding StartDate}" 
    DisplayDateEnd="{Binding EndDate}" 
    IsEnabled="{Binding IsEditing}" 
    BlackoutDates="{Binding BlackoutDates, Mode=TwoWay}" />

Option 2: Set the default binding mode to TwoWay in your code behind.

 public static readonly DependencyProperty BlackoutDatesProperty =
    DependencyProperty.RegisterAttached("BlackoutDates", typeof(List<DateTime>),
    typeof(DependencyPropertyHost), 
    new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,  OnBlackoutDatesChanged));
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top