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!

Thursday, 19 May 2011

Automatic AJAX-ification of forms

I've only been doing ASP.net MVC/jQuery a few months but this came as a real "a-ha!" moment to me. To display an AJAX jQuery UI dialog instead of navigating to a new page, simply adorn your links with a class such as "open-dialog" and give them a title attribute:
    Do Something
then in your script, you can simply use this call:
    $('a.open-dialog').click(function () {
        var title = $(this).attr('title');

        $('<div><div>).dialog({
            buttons: {
                OK: function () {
                    $('form', this).submit();
                    $(this).dialog('close');
                },
                Cancel: function () {
                    $(this).dialog('close');
                }
            },
            modal: true,
            title: title
        }).load($(this).attr('href'));

        return false;
    });
This will display a dialog with the contents and title of the link instead of navigating to a new page. Make sure that in your controller you call Request.IsAjaxRequest() and if this returns true, return a Partial containing only the content to show in the dialog.

Monday, 20 September 2010

System.IO.Packaging is broken

I have a file format that is made up of a number of files zipped up, kind of similar to the .docx file format. My application had been using the Ionic.Zip library to read and write those files, quite happily I must say.

I recently stumbled upon the System.IO.Packaging namespace and realised that as it's part of the .NET framework I could remove one more dependency in my application, great!

However after just a little experimentation I find that my optimism was misplaced. When I tried to open one of my existing .zip packaged files using System.IO.Packaging it couldn't find any files. After some googling it transpires that a [Content Types].xml file is required, for some reason, in order for System.IO.Packaging to be able to find anything in the zip file.

No problem I thought, I'll just create a dummy xml file, which should create that weird [Content Types].xml file and add the XML mime type to it, right?

Well yeah... that's what it does. Except it also deletes all of the existing xml files in the package!

WTF?!? Surely there must be a way to do this? Anyone?

Tuesday, 31 August 2010

WPF 3D: Translating a 2D point into 3D world-space

Although WPF 3D is generally a great framework for doing simple 3D work, there are times when you want more facilities than the framework gives you up-front.

An example: WPF gives us the VisualTreeHelper.HitTest() method which takes a 2D coordinate and can be used to hit-test a 3D model. That's great as far as it goes, but if you want to do anything slightly more elaborate than a simple hit-test you'll be left scratching your head. What you commonly need in such situations is to be able to turn a 2D point into 3D ray in order to do calculations, but WPF provides no way to do this.

Except it does - it's just not public. The Camera class defines the following method:

internal abstract RayHitTestParameters RayFromViewportPoint(Point point, Size viewSize, Rect3D boundingRect, out double distanceAdjustment);

Because the method is internal you have to use reflection to call it. Looking at the code in Reflector, it seems the 'boundingRect' and 'distanceAdjustment' parameters are only used when you're using an OrthographicCamera, so for a PerspectiveCamera you'll just need to pass it your 2D point and the size of your Viewport3D and you'll receive a RayHitTestParameters object containing the Origin and Direction of the ray.

Here's the wrapper method I use to call it:

RayHitTestParameters RayFromViewportPoint(Viewport3DVisual viewport, Point point)
{
    MethodInfo method = typeof(Camera).GetMethod("RayFromViewportPoint", BindingFlags.NonPublic | BindingFlags.Instance);
    double distanceAdjustment = 0;
    object[] parameters = new object[] 
    { 
        point, viewport.Viewport.Size, null, distanceAdjustment 
    };

    return (RayHitTestParameters)method.Invoke(viewport.Camera, parameters);
}

Wednesday, 21 July 2010

MVVM and Multiple Selection – Part III

Update: Feb 2nd 2012 - I've updated this code to work with DataGrid.

Download the source code for this article

In the previous two blog posts in this series I explored the current best practises for dealing with multiple selections in MVVM, and explained the problems one may encounter.

My feeling for a long time has been “What I really need is a CollectionView that supports multiple selections.” So I decided to write one, and it turned out to be pretty easy!

So without delay, here’s the code for MultiSelectCollectionView:

    public class MultiSelectCollectionView<T> : ListCollectionView, IMultiSelectCollectionView
    {
        public MultiSelectCollectionView(IList list)
            : base(list)
        {
            SelectedItems = new ObservableCollection<T>();
        }

        void IMultiSelectCollectionView.AddControl(Selector selector)
        {
            this.controls.Add(selector);
            SetSelection(selector);
            selector.SelectionChanged += control_SelectionChanged;
        }

        void IMultiSelectCollectionView.RemoveControl(Selector selector)
        {
            if (this.controls.Remove(selector))
            {
                selector.SelectionChanged -= control_SelectionChanged;
            }
        }

        public ObservableCollection<T> SelectedItems { get; private set; }

        void SetSelection(Selector selector)
        {
            MultiSelector multiSelector = selector as MultiSelector;
            ListBox listBox = selector as ListBox;

            if (multiSelector != null)
            {
                multiSelector.SelectedItems.Clear();

                foreach (T item in SelectedItems)
                {
                    multiSelector.SelectedItems.Add(item);
                }
            }
            else if (listBox != null)
            {
                listBox.SelectedItems.Clear();

                foreach (T item in SelectedItems)
                {
                    listBox.SelectedItems.Add(item);
                }
            }
        }

        void control_SelectionChanged(object sender, SelectionChangedEventArgs e)
        {
            if (!this.ignoreSelectionChanged)
            {
                bool changed = false;

                this.ignoreSelectionChanged = true;

                try
                {
                    foreach (T item in e.AddedItems)
                    {
                        if (!SelectedItems.Contains(item))
                        {
                            SelectedItems.Add(item);
                            changed = true;
                        }
                    }

                    foreach (T item in e.RemovedItems)
                    {
                        if (SelectedItems.Remove(item))
                        {
                            changed = true;
                        }
                    }

                    if (changed)
                    {
                        foreach (Selector control in this.controls)
                        {
                            if (control != sender)
                            {
                                SetSelection(control);
                            }
                        }
                    }
                }
                finally
                {
                    this.ignoreSelectionChanged = false;
                }
            }
        }

        bool ignoreSelectionChanged;
        List<Selector> controls = new List<Selector>();
    }

As you can see, it’s pretty simple. The class derives from ListCollectionView and maintains an ObservableCollection to hold the selected items and a List to hold the controls that are bound to it.

One thing to notice is that the class is generic, in order to allow the SelectedItems collection to be strongly typed. This may seem like a strange choice considering that CollectionViews are usually not strongly typed.

The reason for this is that the items in a CollectionView will usually only be used by the View side of the application and view controls do not care about having strongly typed values. However, the new SelectedItems collection will usually be used from the ViewModel side of things, where having strongly typed values is important.

Now, WPF controls have no knowledge of MultiSelectCollectionView so if you hook it up to an ItemsControl now, it will work exactly the same as a ListCollectionView, with no multi-select support. We add in multi-select support using the magic of attached properties:

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

        public static bool GetIsEnabled(Selector target)
        {
            return (bool)target.GetValue(IsEnabledProperty);
        }

        public static void SetIsEnabled(Selector target, bool value)
        {
            target.SetValue(IsEnabledProperty, value);
        }

        public static readonly DependencyProperty IsEnabledProperty =
            DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(MultiSelect), 
                new UIPropertyMetadata(IsEnabledChanged));

        static void IsEnabledChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            Selector selector = sender as Selector;
            IMultiSelectCollectionView collectionView = selector.ItemsSource as IMultiSelectCollectionView;

            if (selector != null && collectionView != null)
            {
                if ((bool)e.NewValue)
                {
                    collectionView.AddControl(selector);
                }
                else
                {
                    collectionView.RemoveControl(selector);
                }
            }
        }

        static void ItemsSourceChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            Selector selector = sender as Selector;

            if (GetIsEnabled(selector))
            {
                IMultiSelectCollectionView oldCollectionView = e.OldValue as IMultiSelectCollectionView;
                IMultiSelectCollectionView newCollectionView = e.NewValue as IMultiSelectCollectionView;

                if (oldCollectionView != null)
                {
                    oldCollectionView.RemoveControl(selector);
                }

                if (newCollectionView != null)
                {
                    newCollectionView.AddControl(selector);
                }
            }
        }
    }

Here we create an attached property, MultiSelect.IsEnabled. When set to true on a control derived from Selector, it adds the control to the MultiSelectCollectionView’s list of bound controls.

We also override the metadata for the ItemsSource property for all Selector controls. In this way we can detect when a control’s ItemsSource changes and ensure that the control is registered with the correct MultiSelectCollectionView. Note that because MultiSelectCollectionView is generic, we use the non-generic IMultiSelectCollectionView interface to add/remove controls. IMultiSelectCollectionView looks like this:

    public interface IMultiSelectCollectionView
    {
        void AddControl(Selector selector);
        void RemoveControl(Selector selector);
    }

Now we can add multiple selection capabilites to a control like so:

<ListBox ItemsSource="{Binding Items}" SelectionMode="Extended" local:MultiSelect.IsEnabled="True"/>

That’s it! You can download the source code here. Let me know of any problems you find and also any improvements!