2011年7月29日金曜日

Binding ActualHeight and ActualWidth (Silverlight)

As reported to Microsoft Connect:

  Silverlight Grid/StackPanel/Canvas ActualHeight and AcutalWidth is 0
  http://connect.microsoft.com/VisualStudio/feedback/details/522231/silverlight-grid-stackpanel-canvas-actualheight-and-acutalwidth-is-0

and as described in the API document:

  FrameworkElement.ActualHeight Property
  http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.actualheight%28v=VS.95%29.aspx

  FrameworkElement.ActualWidth Property
  http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.actualwidth%28v=VS.95%29.aspx

ActualHeight and ActualWidth are not suitable for data binding. Therefore, as a workaround, Microsoft suggests that SizeChanged event be used if we want to do something when actual size changes. However, all we want are just data-binding-suitable ActualHeight and ActualWidth, and here I show an implementation for that purpose.

The class, SizeChange, whose implementation I show later, defines 3 attached properties named "IsEnabled", "ActualHeight" and "ActualWidth", respectively. If SizeChange.IsEnabled="True" is given as a property to a FrameworkElement, actual height and actual width of the FrameworkElement can be obtained through SizeChange.ActualHeight and SizeChange.ActualWidth.

Before diving into the implementation, let's see a usage example to understand what we want to realize.

Usage Example:

<...
    // At an appropriate place, define a namespace prefix for
    // the namespace under whicn SizeChange class is contained.
    //
    // The definition below assumes that SizeChange class exists
    // under the namespace "MyApp.UI" and specifies "local_ui"
    // as a prefix for the namespace.
    xmlns:local_ui="clr-namespace:MyApp.UI"
    >

    // Give a SizeChange.IsEnabled="True" to a FrameworkElement.
    // By doing this, actual height and actual width of this
    // element can be bound through SizeChange.ActualHeight and
    // SizeChange.ActualWidth.
    <Grid local_ui:SizeChange.IsEnabled="True" x:Name="grid1">
        ...
    </Grid>

    // Bind the data-binding-suitable attached property.
    //
    // Here, Text property of the TextBlock instance is bound
    // to SizeChange.ActualHeight attached-property of the Grid
    // instance named "grid1".
    //
    // As a result, The TextBlock shows the actual height of
    // the Grid instance, and the shown value is refreshed
    // whenever the actual size of the Grid instance is changed.
    //
    // Note that "Path=(local_ui:SizeChange.ActualHeight)"
    // is used instead of "Path=ActualHeight".
    <TextBlock Text="{Binding ElementName=grid1,
                              Path=(local_ui:SizeChange.ActualHeight)}"/>


And the implementation is here:

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace MyApp.UI
{
    // 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);
        }
    }
}


ActualHeight と ActualWidth をバインドする (Silverlight)

Microsoft Connect に報告されているように、

  Silverlight Grid/StackPanel/Canvas ActualHeight and AcutalWidth is 0
  http://connect.microsoft.com/VisualStudio/feedback/details/522231/silverlight-grid-stackpanel-canvas-actualheight-and-acutalwidth-is-0

また、API ドキュメントに記述されているように、

  FrameworkElement.ActualHeight プロパティ
  http://msdn.microsoft.com/ja-jp/library/system.windows.frameworkelement.actualheight%28v=VS.95%29.aspx

  FrameworkElement.ActualWidth プロパティ
  http://msdn.microsoft.com/ja-jp/library/system.windows.frameworkelement.actualwidth%28v=VS.95%29.aspx

ActualHeight と ActualWidth はデータバインディングに適していない。そのため、回避策として、実際のサイズが変更されたときに何かをしたい場合は SizeChanged イベントを使うようにと、マイクロソフトは言っている。しかし、私たちが欲しいのは、単にデータバインディングに適した ActualHeight と ActualWidth であり、そしてそのための実装をここにあげる。

後で実装を見ることになる SizeChange クラスは、IsEnabled、ActualHeight、ActualWidth という三つのアタッチトプロパティ (attached property) を定義している。SizeChange.IsEnabled="True" がプロパティとして FrameworkElement に与えられると、その FrameworkElement の実際の高さと実際の幅を、SizeChange.ActualHeight と SizeChange.ActualWidth を通じて得ることができるようになる。

実装を見る前に、何を実現しようとしているかを理解するために、使用例を見よう。

使用例:

<...
    // 適切な場所で、SizeChange クラスが含まれる名前空間用の
    // 名前空間プレフィックスを定義する。
    //
    // 下記の定義では、SizeChange クラスが名前空間 MyApp.UI
    // にあるものと仮定し、その名前空間用のプレフィックスと
    // して local_ui を指定している。
    xmlns:local_ui="clr-namespace:MyApp.UI"
    >

    // SizeChange.IsEnabled="True" を FrameworkElement に
    // 与える。これをすることにより、このエレメントの
    // 実際の高さと幅を、SizeChange.ActualHeight と
    // SizeChange.ActualWidth でバインドできるようになる。
    <Grid local_ui:SizeChange.IsEnabled="True" x:Name="grid1">
        ...
    </Grid>

    // データバインディングに適したアタッチトプロパティを
    // バインドする。
    //
    // ここでは、TextBlock インスタンスの Text プロパティを、
    // grid1 という名前の Grid インスタンスのアタッチト
    // プロパティ SizeChange.ActualHeight にバインドしている。
    //
    // 結果として、TextBlock は Grid インスタンスの実際の
    // 高さを表示し、Grid インスタンスの実際のサイズが変更
    // されるたびに表示される値が更新される。
    //
    // "Path=ActualHeight" ではなく
    // "Path=(local_ui:SizeChange.ActualHeight)" が使用されている
    // ことに注意。
    <TextBlock Text="{Binding ElementName=grid1,
                              Path=(local_ui:SizeChange.ActualHeight)}"/>

そして実装は下記のとおり。

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace MyApp.UI
{
    // アタッチトプロパティを登録する必要があるので、SizeChange
    // クラスを DependencyObject のサブクラスとして宣言する。
    public class SizeChange : DependencyObject
    {
        #region Attached property "IsEnabled"

        // IsEnabled プロパティの名前
        public const string IsEnabledPropertyName = "IsEnabled";

        // IsEnabled という名前のアタッチトプロパティを登録する。
        // IsEnabled プロパティの値が変更されると OnIsEnabledChanged
        // メソッドが呼ばれることに注意。
        public static readonly DependencyProperty IsEnabledProperty
            = DependencyProperty.RegisterAttached(
                IsEnabledPropertyName,
                typeof(bool),
                typeof(SizeChange),
                new PropertyMetadata(false, OnIsEnabledChanged));

        // IsEnabled プロパティのゲッター。依存関係システムが
        // 使用するので、このメソッドの名前は変更するべきではない。
        public static bool GetIsEnabled(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsEnabledProperty);
        }

        // IsEnabled プロパティのセッター。依存関係システムが
        // 使用するので、このメソッドの名前は変更するべきではない。
        public static void SetIsEnabled(DependencyObject obj, bool value)
        {
            obj.SetValue(IsEnabledProperty, value);
        }

        #endregion

        #region Attached property "ActualHeight"

        // ActualHeight プロパティの名前
        public const string ActualHeightPropertyName = "ActualHeight";

        // ActualHeight という名前のアタッチトプロパティを登録する。
        // このプロパティの値は、SizeChanged イベントが起こったときに
        // 変更される。
        public static readonly DependencyProperty ActualHeightProperty
            = DependencyProperty.RegisterAttached(
                ActualHeightPropertyName,
                typeof(double),
                typeof(SizeChange),
                null);

        // ActualHeight プロパティのゲッター。依存関係システムが
        // 使用するので、このメソッドの名前は変更するべきではない。
        public static double GetActualHeight(DependencyObject obj)
        {
            return (double)obj.GetValue(ActualHeightProperty);
        }

        // ActualHeight プロパティのセッター。依存関係システムが
        // 使用するので、このメソッドの名前は変更するべきではない。
        public static void SetActualHeight(DependencyObject obj, double value)
        {
            obj.SetValue(ActualHeightProperty, value);
        }

        #endregion

        #region Attached property "ActualWidth"

        // ActualWidth プロパティの名前
        public const string ActualWidthPropertyName = "ActualWidth";

        // ActualWidth という名前のアタッチトプロパティを登録する。
        // このプロパティの値は、SizeChanged イベントが起こったときに
        // 変更される。
        public static readonly DependencyProperty ActualWidthProperty
            = DependencyProperty.RegisterAttached(
                ActualWidthPropertyName,
                typeof(double),
                typeof(SizeChange),
                null);

        // ActualWidth プロパティのゲッター。依存関係システムが
        // 使用するので、このメソッドの名前は変更するべきではない。
        public static double GetActualWidth(DependencyObject obj)
        {
            return (double)obj.GetValue(ActualWidthProperty);
        }

        // ActualWidth プロパティのセッター。依存関係システムが
        // 使用するので、このメソッドの名前は変更するべきではない。
        public static void SetActualWidth(DependencyObject obj, double value)
        {
            obj.SetValue(ActualWidthProperty, value);
        }

        #endregion

        // このメソッドは、IsEnabled プロパティの値が変更されたときに
        // 呼び出される。新しい値が true の場合、対象のエレメントの
        // SizeChanged イベントにイベントハンドラーが追加される。
        private static void OnIsEnabledChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
        {
            // SizeChanged イベントにイベントハンドラーを追加するので、
            // エレメントは FrameworkElement のインスタンスでなければ
            // ならない。
            var element = obj as FrameworkElement;

            if (element == null)
            {
                // 与えられたオブジェクトは FrameworkElement のインス
                // タンスではないので、SizeChanged イベントが無い。
                // そのため、当オブジェクトに対してできることがない。
                return;
            }

            // IsEnabled=True の場合
            if (args.NewValue != null && (bool)args.NewValue == true)
            {
                // エレメントにアタッチする
                Attach(element);
            }
            else
            {
                // エレメントからデタッチする。
                Detach(element);
            }
        }

        private static void Attach(FrameworkElement element)
        {
            // エレメントの実際のサイズが変更されたときに処理を
            // おこなえるよう、エレメントの SizeChanged イベントに
            // イベントハンドラーを追加する。
            element.SizeChanged += HandleSizeChanged;
        }

        private static void Detach(FrameworkElement element)
        {
            // エレメントからイベントハンドラーを取り除く。
            element.SizeChanged -= HandleSizeChanged;
        }

        // SizeChanged イベントが起こった時に実行されるイベントハンドラー
        private static void HandleSizeChanged(object sender, SizeChangedEventArgs args)
        {
            var element = sender as FrameworkElement;

            if (element == null)
            {
                return;
            }

            // 実際の高さと幅を取得する。
            var width  = args.NewSize.Width;
            var height = args.NewSize.Height;

            // SizeChange.ActualHeight と SizeChange.ActualWidth の
            // 値を更新する。
            SetActualWidth(element, width);
            SetActualHeight(element, height);
        }
    }
}