tethered.sun
tethered.sun

Reputation: 159

UserControl with datatemplated ContentControl throws binding errors when its DataContext changes

In my project, I wanted a UserControl that can display different formats of an image (bitmap, svg), selected from a ListBox. The SelectedItem of the ListBox is bound to the appropriate view model, which in turn changes the DataContext of the UserControl, and what I want to achieve is for it to change the displaying control (an Image for bitmaps, a SharpVectors.SvgViewBox for svg files) through data templates. It does so, but raises data binding errors, as if the templates were still intact whilst the UserControl's DataContext has already been changed.

I should like to a) avoid any data binding errors even if they cause no visible problems b) understand what is happening, so I prepared a MWE, which, to my surprise, displays the same behaviour, so I can present it here.

My minimal UserControl is as follows:

<UserControl x:Class="BindingDataTemplateMWE.VersatileControl"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:system="clr-namespace:System;assembly=mscorlib"
             mc:Ignorable="d" 
             d:DesignHeight="450" d:DesignWidth="800">
    <UserControl.Resources>
        <DataTemplate DataType="{x:Type system:String}">
            <TextBlock Text="{Binding Content.Length, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}}" />
        </DataTemplate>
        <DataTemplate DataType="{x:Type system:DateTime}">
            <TextBlock Text="{Binding Content.DayOfWeek, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=ContentControl}}" />
        </DataTemplate>
    </UserControl.Resources>
    <Grid>
        <ContentControl
                Content="{Binding .}" />
    </Grid>
</UserControl>

The MainWindow that references this UserControl has the following XAML:

<Window x:Class="BindingDataTemplateMWE.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:BindingDataTemplateMWE"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*" />
            <ColumnDefinition Width="*" />
        </Grid.ColumnDefinitions>
        <ListBox
            Grid.Column="0"
            SelectedItem="{Binding Selected}"
            ItemsSource="{Binding Items}" />
        <local:VersatileControl
            Grid.Column="1"
            DataContext="{Binding Selected}" />
    </Grid>
</Window>

with the following code-behind (to make the MWE indeed minimal, I made the window its own DataContext, but originally there is a dedicated view model):

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows;

namespace BindingDataTemplateMWE
{
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        private object selected;

        public event PropertyChangedEventHandler PropertyChanged;

        public List<object> Items { get; }

        public object Selected {
            get { return selected; }
            set {
                selected = value;
                PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Selected)));
            }
        }

        public MainWindow()
        {
            InitializeComponent();
            DataContext = this;
            Items = new List<object>() { "a string", DateTime.Now, "another string" };
        }
    }
}

When I select a different item in the list, the desired effect takes place: the UserControl displays the length if a string is selected and the day of week when a DateTime. Still, I get the following binding error when selecting a DateTime after a string:

Length property not found on object of type DateTime.

and conversely, selecting a string after a DateTime yields

DayOfWeek property not found on object of type String.

It is clear that what I am doing is not meant to be done, but I do not know what the correct paradigm is and what happens in the background. Please advise me. Thank you.

Upvotes: 2

Views: 399

Answers (1)

Sinatr
Sinatr

Reputation: 21999

I've seen this problem often when creating complex data templates (several levels of nesting) when views are loaded/unloaded. Honestly, some of such errors I am ignoring completely.

In your case something similar happens because you are manipulating DataContext directly. At the moment the new value is set, the previous value is still used in bindings, which monitor for source change and will try to update the target.

In your scenario you don't need this constant monitoring, so an easy fix is to use BindingMode.OneTime:

<DataTemplate DataType="{x:Type system:String}">
    <TextBlock Text="{Binding Content.Length, RelativeSource={RelativeSource AncestorType=ContentControl}, Mode=OneTime}" />
</DataTemplate>

Upvotes: 2

Related Questions