1. 程式人生 > >[UWP]附加屬性2:實現一個Canvas

[UWP]附加屬性2:實現一個Canvas

5. 附加屬性實踐:自定義Canvas

附加屬性在UWP中是一個十分重要的組成部分,很多功能都依賴於附加屬性實現,典型的例子是常用的Grid和Canvas。通常附加屬性有三個使用場景:插入屬性、觸發行為、當做快取。可以參考以下提供的MyCanvas示例理解這三點。

5.1 插入屬性

這裡實現的MyCanvas繼承自Panel,是一個十分簡單的類(作為示例並沒有十分嚴格的驗證等程式碼,所以只有幾十行程式碼),它實現了和Canvas類似的佈局並且提供了Left和Right兩個附加屬性。使用方式如下:

<local:MyCanvas>
    <Rectangle local:MyCanvas
.Left="50" local:MyCanvas.Top="50" Height="100" Width="100" Fill="Green" /> </local:MyCanvas>

Panel最核心的程式碼是ArrangeOverride,簡單來說,它負責定位Children中的所有元素。MyCanvas讀取子元素的定位資訊MyCanvas.Left和MyCanvas.Top後對其進行定位,子元素自身並沒有這兩個屬性,只有通過附加屬性插入。

public static
double GetLeft(DependencyObject obj) { return (double)obj.GetValue(LeftProperty); } public static void SetLeft(DependencyObject obj, double value) { obj.SetValue(LeftProperty, value); } public static readonly DependencyProperty LeftProperty = DependencyProperty.RegisterAttached("Left"
, typeof(double), typeof(MyCanvas), new PropertyMetadata(0d)); public static double GetTop(DependencyObject obj) { return (double)obj.GetValue(TopProperty); } public static void SetTop(DependencyObject obj, double value) { obj.SetValue(TopProperty, value); } public static readonly DependencyProperty TopProperty = DependencyProperty.RegisterAttached("Top", typeof(double), typeof(MyCanvas), new PropertyMetadata(0d)); protected override Size ArrangeOverride(Size arrangeSize) { foreach (UIElement child in Children) { double left = GetLeft(child); double top = GetTop(child); child.Arrange(new Rect(new Point(left, top), child.DesiredSize)); } return arrangeSize; }

5.2 觸發行為

ArrangeOverride是MyCanvas被載入到VisualTree上後被呼叫的,想要監視MyCanvas.Left或MyCanvas.Top屬性並在每次更改後觸發ArrangeOverride更改佈局,可以在這兩個屬性的PropertyMetadata中新增PropertyChangedCallback,程式碼如下:

public static readonly DependencyProperty TopProperty =
    DependencyProperty.RegisterAttached("Top", typeof(double), typeof(MyCanvas), new PropertyMetadata(0d, OnLeftChanged));


private static void OnLeftChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    double oldValue = (double)args.OldValue;
    double newValue = (double)args.NewValue;
    if (oldValue == newValue)
        return;

    var parent = VisualTreeHelper.GetParent(obj) as MyCanvas;
    if (parent != null)
        parent.InvalidateArrange();
}

當Left改變時呼叫OnLeftChanged,這裡DependencyObject obj就是被附加了Left屬性的子元素。通過 VisualTreeHelper.GetParent找到它的父元素,呼叫父元素的InvalidateArrange再次觸發ArrangeOverride函式。

5.3 當做快取

有時我會很偷懶地把附加屬性當做快取來用。譬如在上面的程式碼中,假設VisualTreeHelper.GetParent是一個很耗時的操作(只是假設),我會把parent放到快取裡面,而這個快取還是用附加屬性實現的。

private static void OnLeftChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
    double oldValue = (double)args.OldValue;
    double newValue = (double)args.NewValue;
    if (oldValue == newValue)
        return;

    var parent = GetCanvasParent(obj);
    if (parent == null)
    {
        parent = VisualTreeHelper.GetParent(obj) as MyCanvas;
        SetCanvasParent(obj, parent);
    }
    if (parent != null)
        parent.InvalidateArrange();
}

注意: 實際上VisualTreeHelper.GetParent函式並沒有十分耗時,所以這裡是沒必要這樣寫的。

5.4 完整的MyCanvas程式碼

public class MyCanvas : Panel
{
    /// <summary>
    //  從指定元素獲取 Left 依賴項屬性的值。
    /// </summary>
    /// <param name="obj">The element from which the property value is read.</param>
    /// <returns>Left 依賴項屬性的值</returns>
    public static double GetLeft(DependencyObject obj)
    {
        return (double)obj.GetValue(LeftProperty);
    }

    /// <summary>
    /// 將 Left 依賴項屬性的值設定為指定元素。
    /// </summary>
    /// <param name="obj">The element on which to set the property value.</param>
    /// <param name="value">The property value to set.</param>
    public static void SetLeft(DependencyObject obj, double value)
    {
        obj.SetValue(LeftProperty, value);
    }

    /// <summary>
    /// 標識 Left 依賴項屬性。
    /// </summary>
    public static readonly DependencyProperty LeftProperty =
        DependencyProperty.RegisterAttached("Left", typeof(double), typeof(MyCanvas), new PropertyMetadata(0d, OnPositionChanged));

    /// <summary>
    //  從指定元素獲取 Top 依賴項屬性的值。
    /// </summary>
    /// <param name="obj">The element from which the property value is read.</param>
    /// <returns>Top 依賴項屬性的值</returns>
    public static double GetTop(DependencyObject obj)
    {
        return (double)obj.GetValue(TopProperty);
    }

    /// <summary>
    /// 將 Top 依賴項屬性的值設定為指定元素。
    /// </summary>
    /// <param name="obj">The element on which to set the property value.</param>
    /// <param name="value">The property value to set.</param>
    public static void SetTop(DependencyObject obj, double value)
    {
        obj.SetValue(TopProperty, value);
    }

    /// <summary>
    /// 標識 Top 依賴項屬性。
    /// </summary>
    public static readonly DependencyProperty TopProperty =
        DependencyProperty.RegisterAttached("Top", typeof(double), typeof(MyCanvas), new PropertyMetadata(0d, OnPositionChanged));

    /// <summary>
    //  從指定元素獲取 CanvasParent 依賴項屬性的值。
    /// </summary>
    /// <param name="obj">The element from which the property value is read.</param>
    /// <returns>CanvasParent 依賴項屬性的值</returns>
    public static MyCanvas GetCanvasParent(DependencyObject obj)
    {
        return (MyCanvas)obj.GetValue(CanvasParentProperty);
    }

    /// <summary>
    /// 將 CanvasParent 依賴項屬性的值設定為指定元素。
    /// </summary>
    /// <param name="obj">The element on which to set the property value.</param>
    /// <param name="value">The property value to set.</param>
    public static void SetCanvasParent(DependencyObject obj, MyCanvas value)
    {
        obj.SetValue(CanvasParentProperty, value);
    }

    /// <summary>
    /// 標識 CanvasParent 依賴項屬性。
    /// </summary>
    public static readonly DependencyProperty CanvasParentProperty =
        DependencyProperty.RegisterAttached("CanvasParent", typeof(MyCanvas), typeof(MyCanvas), new PropertyMetadata(null));

    private static void OnPositionChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        double oldValue = (double)args.OldValue;
        double newValue = (double)args.NewValue;
        if (oldValue == newValue)
            return;

        var parent = VisualTreeHelper.GetParent(obj) as MyCanvas;
        if (parent != null)
            parent.InvalidateArrange();
    }

    protected override Size ArrangeOverride(Size arrangeSize)
    {
        foreach (UIElement child in Children)
        {
            double left = GetLeft(child);
            double top = GetTop(child);
            child.Arrange(new Rect(new Point(left, top), child.DesiredSize));
        }
        return arrangeSize;
    }


    protected override Size MeasureOverride(Size constraint)
    {
        Size childConstraint = new Size(Double.PositiveInfinity, Double.PositiveInfinity);

        foreach (UIElement child in Children)
        {
            if (child == null) { continue; }
            child.Measure(childConstraint);
        }
        return new Size();
    }
}

這裡的程式碼參考了WPF中的Canvas,有興趣可以看看它的原始碼:Canvas

6. 記憶體回收

前面提過,依賴屬性的值是以所依賴的物件及屬性標識作為Key存放到HashTable中,附加屬性作為依賴屬性的一種特殊形式它的實現也是這樣。既然這個HashTable一直存在,會不會作為Key的依賴物件也被迫存活,沒有被回收?假設真是這樣的話,設定了Grid.Row、Canvas.Left等屬性的所有物件都被迫存活在記憶體中?
實際上並不需要擔心這個問題,微軟提供了名為ConditionalWeakTable的類並使用這個類實現依賴屬性機制,保證了依賴屬性的記憶體回收。

參考這段程式碼:

 public sealed partial class MainPage : Page
{
    public MainPage()
    {
        this.InitializeComponent();
        Loaded += MainPage_Loaded;
        var button = new MyButton();
        Test test = new Test();
        button.SetValue(Test.AttachedObjectProperty, test);
        this.LayoutRoot.Children.Add(button);
    }

    private void MainPage_Loaded(object sender, RoutedEventArgs e)
    {
        LayoutRoot.Children.Clear();
        Task.Factory.StartNew(async () =>
        {
            while (true)
            {
                await Task.Delay(TimeSpan.FromSeconds(1));
                GC.Collect();
            }
        });
    }
}

public class MyButton : Button
{
    ~MyButton()
    {
        Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss fff:") + "MyButton Finalize");
    }
}

public class Test : DependencyObject
{
    /// <summary>
    //  從指定元素獲取 AttachedObject 依賴項屬性的值。
    /// </summary>
    /// <param name="obj">The element from which the property value is read.</param>
    /// <returns>AttachedObject 依賴項屬性的值</returns>
    public static Test GetAttachedObject(DependencyObject obj)
    {
        return (Test)obj.GetValue(AttachedObjectProperty);
    }

    /// <summary>
    /// 將 AttachedObject 依賴項屬性的值設定為指定元素。
    /// </summary>
    /// <param name="obj">The element on which to set the property value.</param>
    /// <param name="value">The property value to set.</param>
    public static void SetAttachedObject(DependencyObject obj, Test value)
    {
        obj.SetValue(AttachedObjectProperty, value);
    }

    /// <summary>
    /// 標識 AttachedObject 依賴項屬性。
    /// </summary>
    public static readonly DependencyProperty AttachedObjectProperty =
        DependencyProperty.RegisterAttached("AttachedObject", typeof(Test), typeof(Test), new PropertyMetadata(null));

    ~Test()
    {
        Debug.WriteLine(DateTime.Now.ToString("HH:mm:ss fff:") + "Test Finalize");
    }
}

執行後輸出:

02:06:14 741:MyButton Finalize
02:06:14 747:Test Finalize

可以看出在MyButton及附加的Test物件都被確實被回收了。

7. 參考

附加屬性概述
自定義附加屬性
Silverlight附加屬性概述
Silverlight自定義的附加屬性