Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I bind to an attached property in a Style.Resource?

Tags:

c#

.net

wpf

xaml

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?

like image 816
glasswall Avatar asked Nov 29 '12 17:11

glasswall


People also ask

Why should I use attached properties?

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.

How do I use attached properties to select an object?

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.

What are attached properties in XAML?

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.

What are the downsides of attached properties?

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.


2 Answers

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:

  • You are setting the Background property on the TextBox to a VisualBrush instance.
  • The Visual of that brush is not mapped into the Visual Tree of the control.
  • As a result, any {RelativeSource FindAncestor} navigation will fail, as the parent of that visual will be null.
  • This is the case regardless of whether it is declared as a Style or a ControlTemplate.
  • All that said, relying on 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}" />
like image 102
JerKimball Avatar answered Nov 01 '22 07:11

JerKimball


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...

like image 44
Raúl Otaño Avatar answered Nov 01 '22 06:11

Raúl Otaño