Marcel
Marcel

Reputation: 1074

Namespace issue: Attached Dependency Property not found

I have a simple Attached Dependency Property that should offer a mouse over color for my custom controls:

public class Ext
{
    public static readonly DependencyProperty HighlightBrushProperty = DependencyProperty.RegisterAttached("HighlightBrush", typeof(Brush), typeof(Ext), new PropertyMetadata(default(Brush)));

    public static void SetHighlightBrush(DependencyObject element, Brush value)
    {
        element.SetValue(HighlightBrushProperty, value);
    }

    public static Brush GetHighlightBrush(DependencyObject element)
    {
        return (Brush) element.GetValue(HighlightBrushProperty);
    }
}

I'm using it like this in my Generic.xaml:

<Style TargetType="{x:Type local:MyButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Button}">
                <Border x:Name="Border" 
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}">
                    <ContentPresenter />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter TargetName="Border" Property="Background" Value="{Binding (local:Ext.HighlightBrush), RelativeSource={RelativeSource TemplatedParent}}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

But now I must use the same namespace in the client:

<Window ...
        xmlns:local="clr-namespace:Eval.Wpf.AttachedProperty.Controls;assembly=Eval.Wpf.AttachedProperty.Controls">
    <StackPanel>
        <local:MyButton Background="LightGray" local:Ext.HighlightBrush="DarkOrange" Content=" - Press me - " />
    </StackPanel>
</Window>

Changing the namespace to something else, like <c:MyButton Background="LightGray" c:Ext.HighlightBrush="DarkOrange" Content=" - Press me - " /> ends up in

System.Windows.Data Error: 40 : BindingExpression path error: '(local:Ext.HighlightBrush)' property not found on 'object' ''MyButton' (Name='')'. BindingExpression:Path=(local:Ext.HighlightBrush); DataItem='MyButton' (Name=''); target element is 'Border' (Name='Border'); target property is 'Background' (type 'Brush')

Why's using attached properties not type safe? How can I workaround this issue?

Upvotes: 0

Views: 1091

Answers (1)

Grx70
Grx70

Reputation: 10339

Solution

As mentioned in my comment, the solution in your case is to use the Binding.Path property explicitly, i.e.:

{Binding Path=(local:Ext.HighlightBrush), ...}

Diagnosis

I'm not really sure of what are the reasons behind this behavior but I have done some tests and will share the results and observations.

First of all here's a concise example illustrating the issue (excerpt from a Window XAML definition):

<Window.Resources>
    <ControlTemplate x:Key="T1" xmlns:foo="clr-namespace:FooBar">
        <TextBlock Text="{Binding (foo:MainWindow.Test),
                                  RelativeSource={RelativeSource TemplatedParent}}" />
    </ControlTemplate>
</Window.Resources>
<Grid xmlns:bar="clr-namespace:FooBar">
    <Control x:Name="C1" bar:MainWindow.Test="test" Template="{StaticResource T1}" />
</Grid>

We end up with a binding error with the following message:

System.Windows.Data Error: 40 : BindingExpression path error:
    '(foo:MainWindow.Test)' property not found on 'object' ''Control' (Name='C1')'.
    BindingExpression:Path=(foo:MainWindow.Test);
    DataItem='Control' (Name='C1');
    target element is 'TextBlock' (Name='');
    target property is 'Text' (type 'String')

Of course if we use the Binding.Path property everything works as it should.

Now if we set the PresentationTraceSource.TraceLevel="High" on the binding we can see a (crucial, in my opinion) difference in the output - in the former case we get

System.Windows.Data Warning: 58 :   Path: '(foo:MainWindow.Test)'

but in the latter case we get

System.Windows.Data Warning: 58 :   Path: '(0)'

So it seems that using the first syntax is equivalent to instantiating the path with

new PropertyPath("(foo:MainWindow.Test)")

(I think it is done by the new Binding("(foo:MainWindow.Test)") constructor overload), yet the second one seems to use another overload of the PathProperty constructor:

new PropertyPath("(0)", MainWindow.TestProperty)

Disclaimer What I am going to say next is only my hypothesis, so it might just be dead wrong.

It looks like the "(foo:MainWindow.Test)" path is treated like an CLR accessor and is only resolved when there's a source for the binding available. In this case the source is the C1 control. The framework is clever enough to know in what part of the XAML the control is defined, so it tries to resolve the attached property using that context. But in our case the foo prefix is not defined in that context, so the resolution fails, hence the binding fails. One observation that seems to confirm that hypothesis is that if you define said prefix, even if on the control itself:

<Control x:Name="C1" xmlns:foo="clr-namespace:FooBar" (...) />

the binding starts to work as expected. Also, if we modify our template:

<ControlTemplate x:Key="T1">
    <TextBlock Text="{Binding (bar:MainWindow.Test),
                              RelativeSource={RelativeSource TemplatedParent}}" />
</ControlTemplate>

even though the bar prefix is undefined in this context, and the XAML designer complains about it, it still does work in runtime.

The reason why the second approach works is that the property is resolved upon instantiating the Binding - it has to be resolved at this point in order to be passed to the PropertyPath constructor. And since in the context of the Binding in XAML the foo prefix is defined, the resolution succeeds and everything works fine.

In conclusion, it seems that these two cases differ only by the time at which the framework tries to resolve the (foo:MainWindow.Test) string to an actual property, but that isn't only a difference, it is the difference.

Upvotes: 4

Related Questions