I'm trying to create a prompt text label in the background of a TextBox using attached properties, but I can't resolve the binding to the text caption in a style resource:
Style definition:
<Style x:Key="CueBannerTextBoxStyle"
TargetType="TextBox">
<Style.Resources>
<VisualBrush x:Key="CueBannerBrush"
AlignmentX="Left"
AlignmentY="Center"
Stretch="None">
<VisualBrush.Visual>
<Label Content="{Binding Path=(EnhancedControls:CueBannerTextBox.Caption), RelativeSource={RelativeSource AncestorType={x:Type TextBox}}}"
Foreground="LightGray"
Background="White"
Width="200" />
</VisualBrush.Visual>
</VisualBrush>
</Style.Resources>
<Style.Triggers>
<Trigger Property="Text"
Value="{x:Static sys:String.Empty}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="Text"
Value="{x:Null}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused"
Value="True">
<Setter Property="Background"
Value="White" />
</Trigger>
</Style.Triggers>
</Style>
Attached property:
public class CueBannerTextBox
{
public static String GetCaption(DependencyObject obj)
{
return (String)obj.GetValue(CaptionProperty);
}
public static void SetCaption(DependencyObject obj, String value)
{
obj.SetValue(CaptionProperty, value);
}
public static readonly DependencyProperty CaptionProperty =
DependencyProperty.RegisterAttached("Caption", typeof(String), typeof(CueBannerTextBox), new UIPropertyMetadata(null));
}
Usage:
<TextBox x:Name="txtProductInterfaceStorageId"
EnhancedControls:CueBannerTextBox.Caption="myCustomCaption"
Width="200"
Margin="5"
Style="{StaticResource CueBannerTextBoxStyle}" />
The idea is that you can define the text prompt used in the visual brush when you create the textbox, but I'm getting a binding error:
System.Windows.Data Error: 4 : Cannot find source for binding with reference 'RelativeSource FindAncestor, AncestorType='System.Windows.Controls.TextBox', AncestorLevel='1''. BindingExpression:Path=(0); DataItem=null; target element is 'Label' (Name=''); target property is 'Content' (type 'Object')
The code works fine if I just hardcode the Label.Content property in the style.
Any ideas?
Because you can use Attached Properties to attach both behavior and property values, they’re a simple way to go, and there are no limitations when compared with other techniques.
Using Attached Properties, you can simply define a Boolean Attached Property called "SelectOnEntry" and create a change handler that’s triggered whenever the property is set to true. In that handler, you grab a reference to the object, and subscribe to its GotFocus event so you can use it to set the selection.
Conclusion Attached Properties are a relatively trivial feature in XAML. They are simple syntactical sugar for associating named values with objects. Due to this simplicity, the feature often goes overlooked.
The only downside to Attached Properties is that they are somewhat harder to discover than regular properties. This is a factor that you may want to consider, but in my experience, many alternative techniques (such as subclassing or global event handlers) are no easier to discover.
The problem here has to do with the way Style
works: basically, one "copy" of the Style
will be created (at first reference), and at that point, there may be multiple TextBox
controls you want this Style
applied to - which one will it use for the RelativeSource?
The (probable) answer is to use a Template
instead of a Style
- with a control or data template, you'll be able to access the visual tree of the TemplatedParent
, and that should get you where you need to be.
EDIT: On further thought, I may be incorrect here...I'll throw together a quick test harness when I'm back in front of a computer and see if I can prove/disprove this.
FURTHER EDIT: While what I originally said was arguably "true", that's not your problem; What Raul said re: the visual tree is correct:
Background
property on the TextBox
to a VisualBrush
instance.Visual
of that brush is not mapped into the Visual Tree of the control.{RelativeSource FindAncestor}
navigation will fail, as the parent of that visual will be null.Style
or a ControlTemplate
.ElementName
definitely is non-ideal, as it reduces the reusability of the definition.So, what to do?
I've been wracking my brain overnight trying to think of a way to marshal over the proper inheritance context to the contained brush, with little success...I did come up with this super-hacky way, however:
First, the helper property (note: I don't usually style my code this way, but trying to save space):
public class HackyMess
{
public static String GetCaption(DependencyObject obj)
{
return (String)obj.GetValue(CaptionProperty);
}
public static void SetCaption(DependencyObject obj, String value)
{
Debug.WriteLine("obj '{0}' setting caption to '{1}'", obj, value);
obj.SetValue(CaptionProperty, value);
}
public static readonly DependencyProperty CaptionProperty =
DependencyProperty.RegisterAttached("Caption", typeof(String), typeof(HackyMess),
new FrameworkPropertyMetadata(null));
public static object GetContext(DependencyObject obj) { return obj.GetValue(ContextProperty); }
public static void SetContext(DependencyObject obj, object value) { obj.SetValue(ContextProperty, value); }
public static void SetBackground(DependencyObject obj, Brush value) { obj.SetValue(BackgroundProperty, value); }
public static Brush GetBackground(DependencyObject obj) { return (Brush) obj.GetValue(BackgroundProperty); }
public static readonly DependencyProperty ContextProperty = DependencyProperty.RegisterAttached(
"Context", typeof(object), typeof(HackyMess),
new FrameworkPropertyMetadata(default(HackyMess), FrameworkPropertyMetadataOptions.OverridesInheritanceBehavior | FrameworkPropertyMetadataOptions.Inherits));
public static readonly DependencyProperty BackgroundProperty = DependencyProperty.RegisterAttached(
"Background", typeof(Brush), typeof(HackyMess),
new UIPropertyMetadata(default(Brush), OnBackgroundChanged));
private static void OnBackgroundChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
var rawValue = args.NewValue;
if (rawValue is Brush)
{
var brush = rawValue as Brush;
var previousContext = obj.GetValue(ContextProperty);
if (previousContext != null && previousContext != DependencyProperty.UnsetValue)
{
if (brush is VisualBrush)
{
// If our hosted visual is a framework element, set it's data context to our inherited one
var currentVisual = (brush as VisualBrush).GetValue(VisualBrush.VisualProperty);
if(currentVisual is FrameworkElement)
{
(currentVisual as FrameworkElement).SetValue(FrameworkElement.DataContextProperty, previousContext);
}
}
}
// Why can't there be just *one* background property? *sigh*
if (obj is TextBlock) { obj.SetValue(TextBlock.BackgroundProperty, brush); }
else if (obj is Control) { obj.SetValue(Control.BackgroundProperty, brush); }
else if (obj is Panel) { obj.SetValue(Panel.BackgroundProperty, brush); }
else if (obj is Border) { obj.SetValue(Border.BackgroundProperty, brush); }
}
}
}
And now the updated XAML:
<Style x:Key="CueBannerTextBoxStyle"
TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="TextBox.Text"
Value="{x:Static sys:String.Empty}">
<Setter Property="local:HackyMess.Background">
<Setter.Value>
<VisualBrush AlignmentX="Left"
AlignmentY="Center"
Stretch="None">
<VisualBrush.Visual>
<Label Content="{Binding Path=(local:HackyMess.Caption)}"
Foreground="LightGray"
Background="White"
Width="200" />
</VisualBrush.Visual>
</VisualBrush>
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsKeyboardFocused"
Value="True">
<Setter Property="local:HackyMess.Background"
Value="White" />
</Trigger>
</Style.Triggers>
</Style>
<TextBox x:Name="txtProductInterfaceStorageId"
local:HackyMess.Caption="myCustomCaption"
local:HackyMess.Context="{Binding RelativeSource={RelativeSource Self}}"
Width="200"
Margin="5"
Style="{StaticResource CueBannerTextBoxStyle}" />
<TextBox x:Name="txtProductInterfaceStorageId2"
local:HackyMess.Caption="myCustomCaption2"
local:HackyMess.Context="{Binding RelativeSource={RelativeSource Self}}"
Width="200"
Margin="5"
Style="{StaticResource CueBannerTextBoxStyle}" />
The problem is that the Label
inside the VisualBrush
is not a visual child of the TextBox
, that is the reason why that binding doesn't work. My solution for that problem will be using the ElementName
binding. But the visual brush you are creating is inside a Style
's dictionary resource and then the ElementName
binding will not work because doesn't find the element id. The solution for that will be to create the VisualBrush
in a global dictionary resources. See this XAML code for delcaring the VisualBrush
:
<Window.Resources>
<VisualBrush x:Key="CueBannerBrush"
AlignmentX="Left"
AlignmentY="Center"
Stretch="None">
<VisualBrush.Visual>
<Label Content="{Binding Path=(EnhancedControls:CueBannerTextBox.Caption), ElementName=txtProductInterfaceStorageId}"
Foreground="#4F48DD"
Background="#B72121"
Width="200"
Height="200" />
</VisualBrush.Visual>
</VisualBrush>
<Style x:Key="CueBannerTextBoxStyle"
TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Text"
Value="{x:Static System:String.Empty}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="Text"
Value="{x:Null}">
<Setter Property="Background"
Value="{DynamicResource CueBannerBrush}" />
</Trigger>
<Trigger Property="IsKeyboardFocused"
Value="True">
<Setter Property="Background"
Value="White" />
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
This code should works. No more code need to be changed so I'm not rewriting all the code.
Hope this solution works for you...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With