Archive for July, 2010

ProportionalPanel for Silverlight


I wanted a panel that distributes its children according to the proportion of their width or by a proportion supplied by the object attached to the DataContext.  This lead to me writing PropotionalPanel – the first custom panel I have written.  It’s useful in building dashboards and other layouts that require collections of children, because Grids aren’t easy to use inside ItemsControls.  I wrote this control to be used as the ItemsPanel of a list box and it works well in that context.When writing a panel the two important routines are MeasureOverride and ArrangeOverride.  There’s lots of documentation on these in the help, but it’s interesting to see the parameters with which these methods are called at different times.  

For instance, even if you have a fixed size container, MeasureOverride is called with infinite dimensions on the first occasion, thereafter the call specifies the fixed size.  

Here’s my version of the MeasureOverride routine. I maintain a variable called totalProportion which is either the sum of the proportions of the attached objects, or the sum of the proportions of the stretched dimension based on orientation.  

  

        protected override Size MeasureOverride(Size availableSize)
        {
            Size finalSize = new Size(availableSize.Width, availableSize.Height);
            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            {  

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            }  

            double sizeAvailable, maxAlternate = 0;
            switch (Orientation)
            {
                case Orientation.Horizontal:
                    sizeAvailable = availableSize.Width;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Width;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Height);
                        }
                        finalSize.Width = sizeAvailable;
                        finalSize.Height = maxAlternate;  

                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(Math.Max(0,Math.Floor((sizeAvailable * p)/totalProportion)), finalSize.Height));
                        }
                    }
                    break;
                case Orientation.Vertical:
                    sizeAvailable = availableSize.Height;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Height;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Width);
                        }
                        finalSize.Height = sizeAvailable;
                        finalSize.Width = maxAlternate;
                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(finalSize.Width, Math.Max(0, Math.Floor((sizeAvailable * p) / totalProportion))));
                        }
                    }  

                    break;
            }  
            return finalSize;
        }
    }  

 

First the total proportion is calculated by walking through the children and summing either the proportions from the attached object, or the required size.  

  

            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            {  

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            }  

 

If the parameters of the MeasureOverride call have infinite dimensions I need to measure all of the children for their desired size, if there is a specific size then I need to measure each child providing guidance on how much space they will be allocated.  

  

           case Orientation.Horizontal:
                    sizeAvailable = availableSize.Width;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Width;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Height);
                        }
                        finalSize.Width = sizeAvailable;
                        finalSize.Height = maxAlternate;  

                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(Math.Max(0,Math.Floor((sizeAvailable * p)/totalProportion)), finalSize.Height));
                        }
                    }
                    break;  

 

In the fixed size measurement I use the proportion to calculate the amount of stretched space available to the control.  It is vital that you both measure and arrange children in the correct size – if you don’t measure then it initially appears to work, but strange sizing artefacts occur.  

The arrangement pass is very similar to the fixed size measurement, except you pass a rectangle to the child giving it the layout slot into which it will fit:  

  

            if (Orientation == Orientation.Vertical)
            {
                foreach (var c in Children.OfType<FrameworkElement>())
                {
                    double p = 0;
                    if (c.DataContext is IProportional)
                    {
                        p = (c.DataContext as IProportional).GetProportion();
                    }
                    else
                    {
                        p = c.DesiredSize.Height;
                    }
                    double d = Math.Max(0,Math.Floor((finalSize.Height * p) / totalProportion));
                    c.Arrange(new Rect(0, offset, finalSize.Width, d));
                    offset += d;
                }
            }  

 

The IProportional interface is implemented on objects that are used as DataContexts for the items in my panel:  

  

    public interface IProportional
    {
        double GetProportion();
    } 

 

When you have a proportional panel like this it’s a good idea to provide some way for the user to resize things!  I use a thing called FrameControl in my ListBox’s DataTemplate – this provides sizing thumbs that can change the horizontal and vertical proportions of the underlying DataContext objects. 

 

        private void WidthThumb_OnDragDelta(object sender, DragDeltaEventArgs e)
        {
            var pp = this.FirstVisualAncestorOfType<ProportionalPanel>();
            var thisIndex = pp.Children.IndexOf(this.FirstVisualAncestorOfType<ListBoxItem>());
            var thisItem = DataContext as DashboardItem;
            var nextItem = pp.Children[thisIndex + 1].Cast<FrameworkElement>().DataContext as DashboardItem; 

            Debug.Assert(thisItem != null);
            Debug.Assert(nextItem != null); 

            var move = (e.HorizontalChange * GetProportions()) / pp.ActualWidth;
            if (thisItem.Proportion + move < 0)
                move = - thisItem.Proportion;
            if (nextItem.Proportion - move < 0)
                move = nextItem.Proportion;
            thisItem.Proportion += move;
            nextItem.Proportion -= move;
            pp.InvalidateMeasure();
        } 

 

This example uses my FirstVisualAncestorOfType extension method found elsewhere on this blog.  For the FrameControl it finds the ProportionalPanel that owns it then works out what the pixel change in the Thumb means in terms of the change to the arbitrary proportions returning by the IProportional implementation.  If you just used heights then this would be a lot simpler, but harder to store.  Obviously you need a thumb for the sizable dimension, but not both, so I show just the relevant thumb based on the panel’s orientation. 

To build a dashboard I layer panel within panel as you can see in this video – which also shows the thumb action and the ability to react to drag and drop (not allowing containers of the same type to be embedded within containers). 

Here’s the complete code for the panel.  I have the IProportional interface defined in a library that is used by the items I attach to the DataContext, so that they don’t have to reference the Panel – but you might just want to insert it in this namespace. 

 

using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls; 

namespace DashboardRenderers
{
    public class ProportionalPanel : Panel
    {
        public Orientation Orientation
        {
            get
            {
                return (Orientation)GetValue(OrientationProperty);
            }
            set
            {
                SetValue(OrientationProperty, value);
            }
        } 

        // Using a DependencyProperty as the backing store for Orientation.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register("Orientation", typeof(Orientation), typeof(ProportionalPanel), new PropertyMetadata(System.Windows.Controls.Orientation.Vertical)); 

        protected override Size ArrangeOverride(Size finalSize)
        {
            double offset = 0;
            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            { 

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            }
            if (Orientation == Orientation.Vertical)
            {
                foreach (var c in Children.OfType<FrameworkElement>())
                {
                    double p = 0;
                    if (c.DataContext is IProportional)
                    {
                        p = (c.DataContext as IProportional).GetProportion();
                    }
                    else
                    {
                        p = c.DesiredSize.Height;
                    }
                    double d = Math.Max(0,Math.Floor((finalSize.Height * p) / totalProportion));
                    c.Arrange(new Rect(0, offset, finalSize.Width, d));
                    offset += d;
                }
            }
            else
            {
                foreach (var c in Children.OfType<FrameworkElement>())
                {
                    double p = 0;
                    if (c.DataContext is IProportional)
                    {
                        p = (c.DataContext as IProportional).GetProportion();
                    }
                    else
                    {
                        p = c.DesiredSize.Width;
                    }
                    double d = Math.Max(0, Math.Floor((finalSize.Width * p) / totalProportion));
                    c.Arrange(new Rect(offset, 0, d, finalSize.Height));
                    offset += d;
                } 

            }
            return finalSize;
        } 
        protected override Size MeasureOverride(Size availableSize)
        {
            Size finalSize = new Size(availableSize.Width, availableSize.Height);
            double totalProportion = 0;
            foreach (var c in Children.OfType<FrameworkElement>())
            { 

                if (c.DataContext is IProportional)
                {
                    totalProportion += (c.DataContext as IProportional).GetProportion();
                }
                else
                {
                    totalProportion += Orientation == Orientation.Vertical
                                           ? c.DesiredSize.Height
                                           : c.DesiredSize.Width;
                }
            } 

            double sizeAvailable, maxAlternate = 0;
            switch (Orientation)
            {
                case Orientation.Horizontal:
                    sizeAvailable = availableSize.Width;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Width;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Height);
                        }
                        finalSize.Width = sizeAvailable;
                        finalSize.Height = maxAlternate; 

                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(Math.Max(0,Math.Floor((sizeAvailable * p)/totalProportion)), finalSize.Height));
                        }
                    }
                    break;
                case Orientation.Vertical:
                    sizeAvailable = availableSize.Height;
                    if (double.IsNaN(sizeAvailable) || double.IsPositiveInfinity(sizeAvailable))
                    {
                        sizeAvailable = 0;
                        foreach (var c in Children)
                        {
                            c.Measure(availableSize);
                            sizeAvailable += c.DesiredSize.Height;
                            maxAlternate = Math.Max(maxAlternate, c.DesiredSize.Width);
                        }
                        finalSize.Height = sizeAvailable;
                        finalSize.Width = maxAlternate;
                    }
                    else
                    {
                        foreach (var c in Children.OfType<FrameworkElement>())
                        {
                            double p;
                            if (c.DataContext is IProportional)
                            {
                                p = (c.DataContext as IProportional).GetProportion();
                            }
                            else
                            {
                                p = Orientation == Orientation.Vertical
                                                       ? c.DesiredSize.Height
                                                       : c.DesiredSize.Width;
                            }
                            c.Measure(new Size(finalSize.Width, Math.Max(0, Math.Floor((sizeAvailable * p) / totalProportion))));
                        }
                    } 

                    break;
            } 
            return finalSize;
        }
    }
} 

, , , , , , , ,

Leave a comment

Finding a typed visual parent in Silverlight


My application frequently needs to find a parent of a Silverlight element, and due to the nature of popup panels I also sometimes want to know if the element is “logically” connected to another element.  To achieve this I wrote a couple of helper functions that walk the visual tree and return a typed parent.  You can implement a special interface to indicate that there is a logical connection between items that aren’t physically connected to each other in the Visual Tree too if you need to (very helpful with focus issues).


        public static T FirstVisualAncestorOfType<T>(this DependencyObject element) where T : DependencyObject
        {
            if (element == null) return null;
           
            var parent = VisualTreeHelper.GetParent(element) as DependencyObject;
            while (parent != null)
            {
                if (parent is T)
                    return (T)parent;
                if (parent is IBreakVisualParenting)
                {
                    parent = ((IBreakVisualParenting)parent).Parent;
                }
                else
                    parent = VisualTreeHelper.GetParent(parent) as DependencyObject;
            }
            return null;
        }

        public interface IBreakVisualParenting
        {
            DependencyObject Parent { get; }
        }

        public static T LastVisualAncestorOfType<T>(this DependencyObject element) where T : DependencyObject
        {
            T item = null;
            var parent = VisualTreeHelper.GetParent(element) as DependencyObject;
            while (parent != null)
            {
                if (parent is T)
                    item = (T) parent;
                if(parent is IBreakVisualParenting)
                {
                    parent = ((IBreakVisualParenting) parent).Parent;
                }
                else
                    parent = VisualTreeHelper.GetParent(parent) as DependencyObject;
            }
            return item;
        }

, ,

Leave a comment

IsInVisualTree – helper function for determining if a Silverlight item is visible or in the visual tree


Some Silverlight functions, especially those to do with coordinate transforms, tend to throw exceptions if the item you are testing isn’t in the visual tree and it is often interesting to know if an item is presently going to be displayed or not.  The following code uses the standard way of determining this, by walking the visual tree to see if the item is connected to the root visual of the application.  Being in the visual tree doesn’t mean the item is actually visible, this depends on the collapsed state of the element and its parents.


        /// <summary>
        /// Determines if an element is in the visual tree
        /// </summary>
        /// <param name="element">The element.</param>
        /// <returns>
        ///  <c>true</c> if element parameter is in
        ///  visual tree otherwise, <c>false</c>.
        /// </returns>
        public static bool IsInVisualTree(this DependencyObject element)
        {
            return IsInVisualTree(element, Application.Current.RootVisual as DependencyObject);
        }

        public static bool IsInVisualTree(this DependencyObject element, DependencyObject ancestor)
        {
            DependencyObject fe = element;
            while (fe != null)
            {
                if (fe == ancestor)
                {
                    return true;
                }

                fe = VisualTreeHelper.GetParent(fe) as DependencyObject;
            }

            return false;
        }

To test if an item is visible you just walk the parents and check the Visibility state:


        public static bool IsVisible(this FrameworkElement ele, FrameworkElement topParent=null)
        {
            if (!ele.IsInVisualTree()) return false;

            while(ele != topParent && ele != null)
            {
                if (ele.Visibility == Visibility.Collapsed) return false;
               ele = VisualTreeHelper.GetParent(ele) as FrameworkElement;
            }
            return true;
        }

, , , , ,

Leave a comment