Reputation: 1074
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
Reputation: 10339
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), ...}
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