Binding to ActualWidth does not work
Asked Answered
S

8

24

In a Silverlight 3.0 application I'm attempting to create a rectangle in a canvas and have it stretch the whole width of the canvas. I have attempted to do this by binding to the ActualWidth property of a parent container (seem sample below), however while I don't see any binding errors the value is not being bound. The rectangle is not visible as its width is zero. In addition tried binding to the ActualWidth of the canvas that contains my rectangle but this made no difference.

I did find this bug logged on Microsoft Connect but there were no workarounds listed.

Has anyone been able to solve this issue or can they point to solution?

Edit: The original code sample was not accurate of what I'm trying to achieve, updated for more clarity.

<UserControl>
    <Border BorderBrush="White"
            BorderThickness="1"
            CornerRadius="4"
            HorizontalAlignment="Center">
        <Grid x:Name="GridContainer">
            <Rectangle Fill="Aqua"
                       Width="150"
                       Height="400" />
            <Canvas>
                <Rectangle Width="{Binding Path=ActualWidth, ElementName=GridContainer}"
                           Height="30"
                           Fill="Red" />
            </Canvas>

            <StackPanel>
                <!-- other elements here -->
            </StackPanel>
        </Grid>
    </Border>
</UserControl>
Sinker answered 21/10, 2009 at 16:55 Comment(0)
T
33

What are you trying to do that requires you to databind to the ActualWidth property? This is a known issue with Silverlight, and there is no simple workaround.

One thing that could be done is to set up the visual tree in such a way that you do not need to actually set the Width of the Rectangle, and just allow it to stretch to the appropriate size. So in the example above, if you remove the Canvas (or change the Canvas to some other Panel) and leave the Rectangle's HorizontalAlignment set to Stretch, it will take up all of the available width (effectively the Width of the Grid).

However, this may not be possible in your particular case, and it may really be necessary to set up the databinding. It has already been established that this is not possible directly, but with the help of a proxy object, we can set up the required binding. Consider this code:

public class ActualSizePropertyProxy : FrameworkElement, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    public FrameworkElement Element
    {
        get { return (FrameworkElement)GetValue(ElementProperty); }
        set { SetValue(ElementProperty, value); }
    }

    public double ActualHeightValue
    {
        get{ return Element == null? 0: Element.ActualHeight; }
    }

    public double ActualWidthValue
    {
        get { return Element == null ? 0 : Element.ActualWidth; }
    }

    public static readonly DependencyProperty ElementProperty =
        DependencyProperty.Register("Element", typeof(FrameworkElement), typeof(ActualSizePropertyProxy), 
                                    new PropertyMetadata(null,OnElementPropertyChanged));

    private static void OnElementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ((ActualSizePropertyProxy)d).OnElementChanged(e);
    }

    private void OnElementChanged(DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement oldElement = (FrameworkElement)e.OldValue;
        FrameworkElement newElement = (FrameworkElement)e.NewValue;

        newElement.SizeChanged += new SizeChangedEventHandler(Element_SizeChanged);
        if (oldElement != null)
        {
            oldElement.SizeChanged -= new SizeChangedEventHandler(Element_SizeChanged);
        }
        NotifyPropChange();
    }

    private void Element_SizeChanged(object sender, SizeChangedEventArgs e)
    {
        NotifyPropChange();
    }

    private void NotifyPropChange()
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs("ActualWidthValue"));
            PropertyChanged(this, new PropertyChangedEventArgs("ActualHeightValue"));
        }
    }
}

We can use this in xaml as follows:

<Grid x:Name="LayoutRoot">
    <Grid.Resources>
        <c:ActualSizePropertyProxy Element="{Binding ElementName=LayoutRoot}" x:Name="proxy" />
    </Grid.Resources>
    <TextBlock x:Name="tb1" Text="{Binding ActualWidthValue, ElementName=proxy}"  />
</Grid>

So we are Binding TextBlock.Text to the ActualWidthValue on the proxy object. The proxy object in turn provides the ActualWidth of the Element, which is provided by another Binding.

This is not a simple solution to the problem, but it is the best that I can think of for how to databind to ActualWidth.

If you explained your scenario a bit more, it may be possible to come up with a simpler solution. DataBinding may not be required at all; would it be possible to just set the property from code in a SizeChanged event handler?

Teredo answered 22/10, 2009 at 2:3 Comment(2)
I took the approach you suggested with the SizeChanged event handler and I'm getting the desired effect. To explain the scenario a little bit more I needed to bind to the ActualWidth property as I have of a bit of an odd UI design that requires some elements to appear outside the bounds of the control.Sinker
More than 5 years old and still works under WinRT ;-) - with one small change: new SizeEventHandler(Element_SizeChanged) has to be replaced by Element_SizeChanged directly.Rossie
B
21

Using the mechanism of attached properties, properties which represent ActualHeight and ActualWidth and are updated by SizeChanged event can be defined. Its usage will look like the following.

<Grid local:SizeChange.IsEnabled="True" x:Name="grid1">...</Grid>

<TextBlock Text="{Binding ElementName=grid1,
                         Path=(local:SizeChange.ActualHeight)}"/>

Technical details can be found at the following:

http://darutk-oboegaki.blogspot.com/2011/07/binding-actualheight-and-actualwidth.html

The advantage of this solution compared to others is in that the attached properties defined in the solution (SizeChange.ActualHeight and SizeChange.ActualWidth) can be used for any FrameworkElement without creating any sub class. This solution is reusable and less invasive.


In the event that the link becomes stale, here is the SizeChange Class as shown on the link:

// Declare SizeChange class as a sub class of DependencyObject

// because we need to register attached properties.
public class SizeChange : DependencyObject
 {
     #region Attached property "IsEnabled"

    // The name of IsEnabled property.
    public const string IsEnabledPropertyName = "IsEnabled";

    // Register an attached property named "IsEnabled".
    // Note that OnIsEnabledChanged method is called when
    // the value of IsEnabled property is changed.
    public static readonly DependencyProperty IsEnabledProperty
         = DependencyProperty.RegisterAttached(
             IsEnabledPropertyName,
             typeof(bool),
             typeof(SizeChange),
             new PropertyMetadata(false, OnIsEnabledChanged));

    // Getter of IsEnabled property. The name of this method
    // should not be changed because the dependency system
    // uses it.
    public static bool GetIsEnabled(DependencyObject obj)
     {
         return (bool)obj.GetValue(IsEnabledProperty);
     }

    // Setter of IsEnabled property. The name of this method
    // should not be changed because the dependency system
    // uses it.
    public static void SetIsEnabled(DependencyObject obj, bool value)
     {
         obj.SetValue(IsEnabledProperty, value);
     }

     #endregion

     #region Attached property "ActualHeight"

    // The name of ActualHeight property.
    public const string ActualHeightPropertyName = "ActualHeight";

    // Register an attached property named "ActualHeight".
    // The value of this property is updated When SizeChanged
    // event is raised.
    public static readonly DependencyProperty ActualHeightProperty
         = DependencyProperty.RegisterAttached(
             ActualHeightPropertyName,
             typeof(double),
             typeof(SizeChange),
             null);

    // Getter of ActualHeight property. The name of this method
    // should not be changed because the dependency system
    // uses it.
    public static double GetActualHeight(DependencyObject obj)
     {
         return (double)obj.GetValue(ActualHeightProperty);
     }

    // Setter of ActualHeight property. The name of this method
    // should not be changed because the dependency system
    // uses it.
    public static void SetActualHeight(DependencyObject obj, double value)
     {
         obj.SetValue(ActualHeightProperty, value);
     }

     #endregion

     #region Attached property "ActualWidth"

    // The name of ActualWidth property.
    public const string ActualWidthPropertyName = "ActualWidth";

    // Register an attached property named "ActualWidth".
    // The value of this property is updated When SizeChanged
    // event is raised.
    public static readonly DependencyProperty ActualWidthProperty
         = DependencyProperty.RegisterAttached(
             ActualWidthPropertyName,
             typeof(double),
             typeof(SizeChange),
             null);

    // Getter of ActualWidth property. The name of this method
    // should not be changed because the dependency system
    // uses it.
    public static double GetActualWidth(DependencyObject obj)
     {
         return (double)obj.GetValue(ActualWidthProperty);
     }

    // Setter of ActualWidth property. The name of this method
    // should not be changed because the dependency system
    // uses it.
    public static void SetActualWidth(DependencyObject obj, double value)
     {
         obj.SetValue(ActualWidthProperty, value);
     }

     #endregion

    // This method is called when the value of IsEnabled property
    // is changed. If the new value is true, an event handler is
    // added to SizeChanged event of the target element.
    private static void OnIsEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
     {
        // The given object must be a FrameworkElement instance,
        // because we add an event handler to SizeChanged event
        // of it.
        var element = obj as FrameworkElement;

         if (element == null)
         {
            // The given object is not an instance of FrameworkElement,
            // meaning SizeChanged event is not available. So, nothing
            // can be done for the object.
            return;
         }

        // If IsEnabled=True
        if (args.NewValue != null && (bool)args.NewValue == true)
         {
             // Attach to the element.
             Attach(element);
         }
         else
         {
             // Detach from the element.
             Detach(element);
         }
     }

     private static void Attach(FrameworkElement element)
     {
        // Add an event handler to SizeChanged event of the element

        // to take action when actual size of the element changes.
        element.SizeChanged += HandleSizeChanged;
     }

     private static void Detach(FrameworkElement element)
     {
        // Remove the event handler from the element.
        element.SizeChanged -= HandleSizeChanged;
     }

    // An event handler invoked when SizeChanged event is raised.
    private static void HandleSizeChanged(object sender, SizeChangedEventArgs args)
     {
         var element = sender as FrameworkElement;

         if (element == null)
         {
             return;
         }

        // Get the new actual height and width.
        var width  = args.NewSize.Width;
         var height = args.NewSize.Height;

        // Update values of SizeChange.ActualHeight and

        // SizeChange.ActualWidth.
        SetActualWidth(element, width);
         SetActualHeight(element, height);
     }
 }
Busybody answered 28/7, 2011 at 16:44 Comment(1)
This works at runtime, but gives me a "value does not fall within the expected range" error in the visual studio designerLundquist
E
9

My solution is to declare my own DependencyProperty called RealWidth and update its value on the SizeChanged event. You can then bind to RealWidth, which will update, unlike the ActualWidth property.

public MyControl()
{
    InitializeComponent();
    SizeChanged += HandleSizeChanged;
}

public static DependencyProperty RealWidthProperty =
     DependencyProperty.Register("RealWidth", typeof (double),
     typeof (MyControl),
     new PropertyMetadata(500D));

public double RealWidth
{
    get { return (double) GetValue(RealWidthProperty); }
    set { SetValue(RealWidthProperty, value); }
}

private void HandleSizeChanged(object sender, SizeChangedEventArgs e)
{
    RealWidth = e.NewSize.Width;
}
Epiphragm answered 14/11, 2010 at 23:34 Comment(1)
I think a variation would be to override OnRenderSizeChanged(); that eliminates adding a handler to the object's own event.Loudmouth
C
5

Why not create a simple panel control that inherits from ContentPresenter and actually can provide the current size.

public class SizeNotifyPanel : ContentPresenter
{
    public static DependencyProperty SizeProperty =
        DependencyProperty.Register("Size",
                                    typeof (Size),
                                    typeof (SizeNotifyPanel),
                                    null);

    public Size Size
    {
        get { return (Size) GetValue(SizeProperty); }
        set { SetValue(SizeProperty, value); }
    }

    public SizeNotifyPanel()
    {
        SizeChanged += (s, e) => Size = e.NewSize;
    }
}

It should then be used as wrapper for the actual content.

<local:SizeNotifyPanel x:Name="Content">
    <TextBlock Text="{Binding Size.Height, ElementName=Content}" />
</local:SizeNotifyPanel>

Worked for me like a charm and looks clean.

Coseismal answered 12/10, 2011 at 8:56 Comment(1)
very well, using StackPanel as base solved my issue. When you use ContentPresenter you have trouble reaching inside elements by name and setting their values.Lucais
T
2

Based on @darutk's answer, here's an attached property-based solution which does the job very elegantly.

public static class SizeBindings
{
    public static readonly DependencyProperty ActualHeightProperty =
        DependencyProperty.RegisterAttached("ActualHeight", typeof (double), typeof (SizeBindings),
                                            new PropertyMetadata(0.0));

    public static readonly DependencyProperty ActualWidthProperty =
        DependencyProperty.RegisterAttached("ActualWidth", typeof (Double), typeof (SizeBindings),
                                            new PropertyMetadata(0.0));

    public static readonly DependencyProperty IsEnabledProperty =
        DependencyProperty.RegisterAttached("IsEnabled", typeof (bool), typeof (SizeBindings),
                                            new PropertyMetadata(false, HandlePropertyChanged));

    private static void HandlePropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var element = d as FrameworkElement;
        if (element == null)
        {
            return;
        }

        if ((bool) e.NewValue == false)
        {
            element.SizeChanged -= HandleSizeChanged;
        }
        else
        {
            element.SizeChanged += HandleSizeChanged;
        }
    }

    private static void HandleSizeChanged(object sender, SizeChangedEventArgs e)
    {
        var element = sender as FrameworkElement;

        SetActualHeight(element, e.NewSize.Height);
        SetActualWidth(element, e.NewSize.Width);
    }

    public static bool GetIsEnabled(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsEnabledProperty);
    }

    public static void SetIsEnabled(DependencyObject obj, bool value)
    {
        obj.SetValue(IsEnabledProperty, value);
    }

    public static Double GetActualWidth(DependencyObject obj)
    {
        return (Double) obj.GetValue(ActualWidthProperty);
    }

    public static void SetActualWidth(DependencyObject obj, Double value)
    {
        obj.SetValue(ActualWidthProperty, value);
    }

    public static double GetActualHeight(DependencyObject obj)
    {
        return (double)obj.GetValue(ActualHeightProperty);
    }

    public static void SetActualHeight(DependencyObject obj, double value)
    {
        obj.SetValue(ActualHeightProperty, value);
    }
}

Use it like this:

    <Grid>
        <Border x:Name="Border" behaviors:SizeBindings.IsEnabled="True"/>
        <Border MinWidth="{Binding (behaviors:SizeBindings.ActualWidth), ElementName=Border}"/>
    </Grid>
Theisen answered 29/3, 2013 at 10:59 Comment(0)
R
1

I've tested the updated xaml that you publishing using a TestConverter to see what value gets passed to the width and it is working for me (I am using VS 2010 B2). To use the TestConverter just set a breakpoint in the Convert method.

    public class TestConverter : IValueConverter
    {

        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return value;
        }

    }

A value of 150 was passed in and the Rectangle had a width of 150.

Were you expecting something different?

Ridglea answered 21/10, 2009 at 18:17 Comment(4)
You are correct about it starting at 0, however since ActualWidth is a dependency property there should be notification when it changes. I cannot bind to Width as it is never set for the control and as a result returns double.NaNSinker
You're correct, it is a dependency property so it should change. However, you are setting the Width in the Xaml above.Ridglea
My apologies, that was sloppy on my end as I just copied the snippet from the sandbox where I was working to resolve the issue I am having. I have fixed the error and updated the sample to better describe my situation.Sinker
I have done as you suggested and my breakpoint is only hit once, the value given in the converter is 0.Sinker
F
0

This is an as an aside answer which may help someone for binding to the ActualWidth.

My process didn't need a change event, it needed an end result of a value in its current state. So I created a dependency property called Target on my custom control/process as a FrameworkElement and the consumer xaml would bind to the actual object in question.

When it was time for the calculation the code could pulled the actual object and extracted it's ActualWidth from it.


Dependency Property on Control

public FrameworkElement Target
{
    get { return (FrameworkElement)GetValue(TargetProperty);}
    set { SetValue(TargetProperty, value);}
}

// Using a DependencyProperty as the backing store for Target.
// This enables animation, styling, binding, general access etc...
public static readonly DependencyProperty TargetProperty =
    DependencyProperty.Register("Target", typeof(FrameworkElement), 
                                typeof(ThicknessWrapper), 
                                new PropertyMetadata(null, OnTargetChanged));

XAML on Consumer side showing a binding to a Rectangle

<local:ThicknessWrapper Target="{Binding ElementName=thePanel}"/>

<Rectangle x:Name="thePanel" HorizontalAlignment="Stretch" Height="20"  Fill="Blue"/>

Code to Acquire

double width;

if (Target != null)
   width = Target.ActualWidth;  // Gets the current value.
Fugger answered 17/7, 2015 at 13:22 Comment(0)
T
0

Based on KeithMahoney's answer, it works fine on my UWP App and solves my problem. However, I cannot see my control in Design time because both the initial values of ActualWidthValue and ActualHeightValue are not provided in Design time. Although it works fine in running time, it is inconvenient for designing the layout of my control. With a little modification, this problem can be solved.

  1. In his c# code for both properties ActualWidthValue and ActualHeightValue, add

    set {;}

    to let us can provide dummy values from XAML code. Although it is no use for running time, it can be used for design time.

  2. In the declaration of Resources of his XAML code, provide c:ActualSizePropertyProxy suitable values for ActualWidthValue and ActualHeightValue such as

    ActualHeightValue="800" ActualWidthValue="400"

    Then it will show you a 400x800 control in design time.

Trish answered 14/10, 2016 at 4:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.