Friday, 17 February 2012

An Accordion Panel in WPF

I recently had need of an Accordian control, such as that found in JQuery UI.

I decided to implement this as a simple Panel. This AccordianPanel can contain any controls, but has special behaviour for child Expander controls: It will only allow a single one to be expanded at any one time.

public AccordianPanel()
{
    AddHandler(Expander.ExpandedEvent, new RoutedEventHandler(ChildExpanded));
}
Starting with the constructor, we register a new event handler for the Expander.ExpandedEvent:
void ChildExpanded(object sender, RoutedEventArgs e)
{
    foreach (UIElement child in InternalChildren)
    {
        Expander expander = FindExpander(child);

        if (expander != null && expander != e.OriginalSource)
        {
            expander.IsExpanded = false;
        }
    }
}
When we detect a child Expander's Expanded state has changed, we look for any other child Expanders and unexpand them. The FindExpander function allows for the Expander to be parented:
Expander FindExpander(UIElement e)
{
    while (e != null && !(e is Expander))
    {
        if (VisualTreeHelper.GetChildrenCount(e) == 1)
        {
            e = VisualTreeHelper.GetChild(e, 0) as UIElement;
        }
        else
        {
            e = null;
        }
    }

    return (Expander)e;
}
Finally, we implement MeasureOverride:
protected override Size MeasureOverride(Size availableSize)
{
    double requiredHeight = 0;
    double resizableHeight = 0;

    foreach (UIElement child in InternalChildren)
    {
        child.Measure(availableSize);
        requiredHeight += child.DesiredSize.Height;

        if (CanResize(child))
        {
            resizableHeight += child.DesiredSize.Height;
        }
    }

    if (requiredHeight > availableSize.Height)
    {
        double pixelsToLose = requiredHeight - availableSize.Height;

        foreach (UIElement child in InternalChildren)
        {
            double height = child.DesiredSize.Height;

            if (CanResize(child))
            {
                height -= (child.DesiredSize.Height / resizableHeight) * pixelsToLose;
                child.Measure(new Size(availableSize.Width, height));
            }
        }
    }

    return base.MeasureOverride(availableSize);
}
And ArrangeOverride:
protected override Size ArrangeOverride(Size finalSize)
{
    double totalHeight = 0;
    double resizableHeight = 0;

    foreach (UIElement child in Children)
    {
        totalHeight += child.DesiredSize.Height;

        if (CanResize(child))
        {
            resizableHeight += child.DesiredSize.Height;
        }
    }

    double pixelsToLose = totalHeight - finalSize.Height;
    double y = 0;

    foreach (UIElement child in InternalChildren)
    {
        double height = child.DesiredSize.Height;

        if (pixelsToLose > 0 && CanResize(child))
        {
            height -= (child.DesiredSize.Height / resizableHeight) * pixelsToLose;
        }

        child.Arrange(new Rect(0, y, finalSize.Width, height));
        y += height;
    }

    return base.ArrangeOverride(finalSize);
}
Plus the small Utility function CanResize:
bool CanResize(UIElement e)
{
    Expander expander = FindExpander(e);
    return expander != null && expander.IsExpanded;
}

And that's it! I initially had problems with the fact that the layout for this type of panel requires two passes, which doesn't immediately seem to be supported by WPF. However, following a question on the ever-reliable StackOverflow, it seems it can in fact be done.

You can even make it animated using something like the AnimatedPanel from here.

Download the code here.

Saturday, 11 February 2012

WebP from .Net

I recently had the need to convert images to Google's .webp format from .net. There is a .net port of the WebP library available, but it didn't seem to work for me and it seems to be no longer maintained.

So P/Invoke it was. Here's the code I used:

Bitmap source = new Bitmap("Desert.jpg");
BitmapData data = source.LockBits(
    new Rectangle(0, 0, source.Width, source.Height),
    ImageLockMode.ReadOnly,
    PixelFormat.Format24bppRgb);
IntPtr unmanagedData;
int size = WebPEncodeBGR(data.Scan0, source.Width, source.Height, data.Stride, 85, out unmanagedData);
byte[] managedData = new byte[size];
Marshal.Copy(unmanagedData, managedData, 0, size);
File.WriteAllBytes("Desert.webp", managedData);

Marshal.FreeHGlobal(unmanagedData); // Doesn't work. How do we free the unmanaged data?

As you can see, all goes swimmingly until it comes to freeing the unmanaged memory allocated by the WebP library. The memory is allocated by malloc() and needs to be freed by calling free(), however you have no access to this from .net.

My solution was to modify the WebP library to provide a WebPFree function, which simply calls the C free(). Calling this instead of Marshal.FreeHGlobal fixes the memory leak.

I've included my modifed WebP binaries as part of a sample project here.

Sunday, 5 February 2012

MVVM and Multiple Selection – Part IV - DataGrid

Download the source for this article.

Previous posts in this series:

Please note, this is WPF only - I don't use Silverlight!

It's been a while since my previous post in this series, MVVM and Multiple Selection – Part III. In the comments to that post, I had a number of people asking about the DataGrid control, however by that point I was no longer in WPF-land and didn't have the time or inclination to investigate properly.

Recently though, I needed to track multiple selections on a DataGrid and as the commenters pointed out, it didn't work. The problem came down to the method I was using to track changes to the ItemsSource property on a control:

static MultiSelect()
{
    Selector.ItemsSourceProperty.OverrideMetadata(typeof(Selector), new FrameworkPropertyMetadata(ItemsSourceChanged));
}

This doesn't work for DataGrid as the ItemsSource metadata on that control is already overridden, so I needed to find a different approach.

Here's what I came up with:

static void IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    Selector selector = sender as Selector;
    bool enabled = (bool)e.NewValue;

    if (selector != null)
    {
        DependencyPropertyDescriptor itemsSourceProperty =
            DependencyPropertyDescriptor.FromProperty(Selector.ItemsSourceProperty, typeof(Selector));
        IMultiSelectCollectionView collectionView = selector.ItemsSource as IMultiSelectCollectionView;

        if (enabled)
        {
            if (collectionView != null) collectionView.AddControl(selector);
            itemsSourceProperty.AddValueChanged(selector, ItemsSourceChanged);
        }
        else
        {
            if (collectionView != null) collectionView.RemoveControl(selector);
            itemsSourceProperty.RemoveValueChanged(selector, ItemsSourceChanged);
        }
    }
}
Instead of overriding the property metadata globally for all Selectors, I attach a listener to the PropertyDescriptor. Now, the only problem here is that the event handler for PropertyDescriptor changed events are plain old EventHandler delegates, so by the time the property has changed we can't get hold of the old IMultiSelectCollectionView in order to remove the control from it. I get round this by storing all attached controls in a static Dictionary.

This leaves us the problem that we're going to be keeping controls alive when after they're gone, but that's a post for another day. In the meantime, download the code!