Ryan Dawson on Longhorn

The software we think, but do not write

Custom Controls in Avalon

Writing a custom control in Avalon is easy, right? Wrong…

On the contrary, it can be just as much work, and in some cases harder. But, on the bright side, I’m pretty sure that it gets easier the longer you plug away.

You might be thinking right now that easiness is the essence of Avalon. And, you’re right. The problem is that as software evolves, we try to build more and more complex things, while maintaining about the same number of lines. To quote Nathan Myhrvold, “Software is a gas -- it expands to fit its container.”

Right off the bat, you are going to notice some things, mainly the fallacy of a custom control. In terms of custom controls, I don’t think they will exist as they once did in WinForms. I’m talking about styling.

If Avalon didn’t get anything else right, I know they got the semantic line-up of design and programming. Think about it, a button is basically a textured link. A listbox is a selectable item list. Panels are for organization of elements and not their look, and so on… The difference is that these controls closely align with their real-world meaning. For example, a listbox is always going to be a list of elements, no matter how that list looks, or what it holds.

So, that’s the first lesson: in Avalon, you pick a control depending on general behavior, and then you make it look however you want. Which I think, is the right way.

 

As an example, to explain my concepts, I am going to create something simple, but usable – a toolbox (like you might see in Photoshop or the like).

Albeit, this is going to be a bit scaled down, so go ahead and fill in the rest later.

First of all, let’s decide what to put in our toolbox. I have chosen a selection and pencil tool. On a side note, I have also decided I want to group my controls. So, here is the design so far:

·         Toolbox

o       Tools Group

§         Selection

§         Pencil

And, as code…

    public class Toolbox

    {

        public ArrayListDataCollection Groups

        {

            get{}

        }

    }

    public class ToolboxGroup

    {

        public string Title

        {

            get{} set{}

        }

        public ArrayListDataCollection Items

        {

            get{}

        }

    }

    public class ToolboxItem

    {

        public DrawingBrush Brush

        {

            get{} set{}

        }

    }

To expose a list to Avalon, the collection must expose IEnumerable. The ArrayListDataCollection has been tailored for UI, but there is also ObservableCollection<type>.

Besides simple text, the only other things I need are some vector images for the tools DrawingBrush. To do this, I use Adobe Illustrator.

 

First, “Save for Web” as SVG graphics. Then, open up the newly created files in notepad. We want to convert SVG to a XAML format that Avalon can recognize.

            <path fill="none" d="M1.515,2.02c1.688,3.375,3.313,6.938,5.188,10.188"/>

<path d="M0.167,2.661c1.697,3.413,3.303,6.872,5.188,10.188c0.944,1.661,3.65,0.396,2.696-1.281 C6.166,8.251,4.56,4.793,2.863,1.379C2.008-0.34 0.692,0.933,0.167,2.661L0.167,2.661z"/>

            To

 

<Path Fill="#000000" Data="M1.515,2.02c1.688,3.375,3.313,6.938,5.188,10.188"/>

<Path Fill="#000000" Data="M0.167,2.661c1.697,3.413,3.303,6.872,5.188,10.188c0.944,1.661,3.65,0.396,2.696-1.281 C6.166,8.251,4.56,4.793,2.863,1.379C2.008-0.34-0.692,0.933,0.167,2.661L0.167,2.661z"/>

So, as you can see, mostly just renaming…On a side note, be sure to copy over the ViewBox from the SVG to your DrawingBrush Viewbox.

 

Moving on, create a file named something like Styles.xaml. This file will serve as our general purpose collection of styles that can later be pluggable – enabling a change of look. But, as we established above, the semantics will be the same – a listbox is still a listbox.

For our root element, we want something called a ResourceDictionary, which is a key value structure that we will later load into the <Application>.Resources (which as you guessed it, is also of type ResourceDictionary).

Go ahead and add the SVG images we created above...

    <DrawingBrush x:Key="Selection" Viewbox="-0.298 -0.105 22 15">

        <DrawingBrush.Drawing>

            <Drawing>

                        ...

            </Drawing>

        </DrawingBrush.Drawing>

    </DrawingBrush>

For each item in the Resource dictionary there is something called a Key. This is how Avalon looks up the Resource to use later, so remember that it’s the magic glue.

Now, just for a second, skip to another file. If you are starting with a Windows Application Project, look for the MyApp.xaml file, or else, open up your Window XAML file.

Add these constructs under the Window tag:

    <Canvas>

<ListBox Width="75" Height="200"

Canvas.Left="20" Canvas.Top="40"/>

    </Canvas>

This is our skeleton -- the ListBox is later to become the toolbox we are building, and the canvas is our workspace (so to speak).

For the data class that we created earlier, we need to hook it up to our listbox. So let’s add a PI Mapping to the top of the Window XAML file (Which lets Avalon know where to find a class referenced in the XAML code).

<?Mapping XmlNamespace="local" ClrNamespace="AvalonApplication1" ?>

And…

<Windowxmlns:l="local" …

You can then reference the Toolbox class in markup by adding this simple line to the Window.Resources. This actually just makes it so that the XAML parser can understand the data structure.

    <Window.Resources>

        <l:Toolbox x:Key="toolBoxData" />

    </Window.Resources>

When working with data binding, there is something that you will see over and over – StaticResource. But, don’t be confused, this has the same meaning that c# has for static references. Create the designated resource only once, and then reuse. Now, let’s bind our Toolbox class to the workspace.

    <Canvas DataContext="{StaticResource toolBoxData}">

        <ListBox Width="75" Height="200"

            Canvas.Left="20" Canvas.Top="40" />

    </Canvas>

A DataContext is a way of saying that any child of Canvas should bind to whatever data we have specified. So, the DataContext is now set to the Toolbox class.

Next, we map the groups in the Toolbox to the ListBox.

<ListBox Width="75" Height="200" Canvas.Left="20" Canvas.Top="40" ItemsSource="{Bind Path=Groups}" />

ItemsSource designates that each item in the list will be bound to the IEnumerable specified. You do this with a simple {Bind Path=XXX} statement. Just remember that you are binding to the DataContext, which means that you must specify a member of the DataContext (Groups in our case).

 

If you run the application, it will look something exactly the opposite of what you expect. The data is actually hooked up correctly, but the ListBox isn’t yet styled to make any meaning of the data.

In your styles.xaml file, let’s add some declarations:

    <Style x:Key="Toolbox">

        <Style.VisualTree>

            <Canvas Background="{StaticResource ToolboxBackground}">

<StackPanel ItemsControl.IsItemsHost="true" Margin="10" Focusable="false" />

            </Canvas>

        </Style.VisualTree>

    </Style>

    <DrawingBrush x:Key="ToolboxBackground">

        <DrawingBrush.Drawing>

            <GeometryDrawing>

                <GeometryDrawing.Brush>

                    <SolidColorBrush Opacity=".6" Color="LightBlue" />

                </GeometryDrawing.Brush>

                <GeometryDrawing.Geometry>

                    <GeometryGroup>

<RectangleGeometry Rect="0,0 1 1" RadiusX=".17" RadiusY=".17" />

                    </GeometryGroup>

                </GeometryDrawing.Geometry>

            </GeometryDrawing>

        </DrawingBrush.Drawing>

    </DrawingBrush>

Go back to the Window XAML file, and change your ListBox to read like this:

<ListBox Style="{StaticResource Toolbox}" Width="75" Height="200" Canvas.Left="20" Canvas.Top="40"

        ItemsSource="{Bind Path=Groups}" />

Now run it…You might notice that the ListBox has changed from its default style to something of a blue rectangle with rounded corners. By setting the Style in the ListBox, we are telling Avalon that we want to handle how the ListBox looks.

Moving on, let’s now tell Avalon how we want each item in the ListBox to look. Here is the ListBox item style, which also represents a group…

[Window XAML file]

<ListBox Style="{StaticResource Toolbox}" Width="75" Height="200" Canvas.Left="20" Canvas.Top="40" ItemStyle="{StaticResource ToolboxGroup}" ItemsSource="{Bind Path=Groups}" />

[Styles.xaml]

    <Style x:Key="ToolboxGroup">

        <Style.VisualTree>

            <StackPanel Focusable="false">

                <Canvas Width="50" />

                <TextBlock TextContent="{Bind Path=Title}" FontFamily="Lucida Sans" FontSize="8.8pt"

                        Foreground="White" TextAlignment="Center" />

                <Rectangle Height="1">

                    <Rectangle.Fill>

                        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0" StatusOfNextUse="Unchangeable">

                            <GradientBrush.GradientStops>

                                <GradientStopCollection StatusOfNextUse="Unchangeable">

                                    <GradientStop Color="transparent" Offset="0" StatusOfNextUse="Unchangeable" />

                                    <GradientStop Color="#cccccc" Offset="0.1" StatusOfNextUse="Unchangeable" />

                                    <GradientStop Color="transparent" Offset="1" StatusOfNextUse="Unchangeable" />

                                </GradientStopCollection>

                            </GradientBrush.GradientStops>

                        </LinearGradientBrush>

                    </Rectangle.Fill>

                </Rectangle>

                <Rectangle Height="1">

                    <Rectangle.Fill>

                        <LinearGradientBrush StartPoint="0,0" EndPoint="1,0" StatusOfNextUse="Unchangeable">

                            <GradientBrush.GradientStops>

                                <GradientStopCollection StatusOfNextUse="Unchangeable">

                                    <GradientStop Color="transparent" Offset="0" StatusOfNextUse="Unchangeable" />

                                    <GradientStop Color="white" Offset="0.1" StatusOfNextUse="Unchangeable" />

                                    <GradientStop Color="transparent" Offset="1" StatusOfNextUse="Unchangeable" />

                                </GradientStopCollection>

                            </GradientBrush.GradientStops>

                        </LinearGradientBrush>

                    </Rectangle.Fill>

                </Rectangle>

<ListBox Style="{StaticResource ToolboxGroupList}" Width="50" Margin="0 7 0 0"

ItemStyle="{StaticResource ToolboxItem}" ItemsSource="{Bind Path=Items}" />

            </StackPanel>

        </Style.VisualTree>

    </Style>

The code gets a little complicated, but just remember that we are creating a title for the group, and then a sub-ListBox. The sub-ListBox is the house for the tools.

A little more code, and we will have the tool items also styled:

    <Style x:Key="ToolboxItem">

        <Style.VisualTree>

<Canvas Width="25" Height="25" Background="{Bind Path=Brush}" />

        </Style.VisualTree>

    </Style>

    <Style x:Key="ToolboxGroupList">

        <Style.VisualTree>

            <!-- Mapping PI always throws exception -->

            <!--<cc:WrapPanel ItemsControl.IsItemsHost="true" />-->

<StackPanel ItemsControl.IsItemsHost="true" Orientation="Horizontal" />

        </Style.VisualTree>

    </Style>

You may have noticed that I created a style for the last ListBox, but there really isn’t anything in it except for a panel. And, remember that panels only lay out controls without dictating look. So, what I am actually doing, is saying “I want the items in the ListBox laid out like this…” In our case – a StackPanel (WrapPanel). Subsequantly, the ItemsControl.IsItemsHost="true" is the glue that tells Avalon to use this panel to host the elements, otherwise, the elements don’t show up.

 

Just to finish out, I’ll explain the WrapPanel, except I will warn you that Avalon always throws an exception, if used in a Style (It’s fine in regular markup).

Panels have a two-pass layout scheme – Measure, and Arrange, in that order.

In the Measure pass, you want to call each child in your panel and tell them how much space you are willing to give them, and they will subsequently measure themselves.

In the Arrange pass, you will call each child in your panel and tell them how much space they are going to get (final).

Since your panel is treated exactly how you treat the children of your panel, it’s obvious to see how things work on a grander scale. In the case of the WrapPanel, it flows elements from left to right, and then wraps to the next line when an item doesn’t fit – pretty simple.

    public class WrapPanel : Panel

    {

    }

Here is the Measure pass:

        private IList<Item> items = null;

        private class Item

        {

            public FrameworkElement Element;

            public double X;

            public double Y;

        }

        protected override Size MeasureOverride(Size availableSize)

        {

            this.items.Clear();

            double x = 0;

            double y = 0;

            double maxY = 0;

            double width = 0;

            double height = 0;

            foreach(FrameworkElement child in this.Children)

            {

                child.Measure(availableSize);

                if (x + child.DesiredSize.Width > availableSize.Width)

                {

                    y += maxY;

                    x = 0;

                    maxY = 0;

                    height += maxY;

                }

                Item item = new Item();

                item.Element = child;

                item.X = x;

                item.Y = y;

                width = Math.Max(x + child.DesiredSize.Width, width);

                maxY = Math.Max(maxY, child.DesiredSize.Height);

            }

            height += maxY;

            width = Math.Max(width, this.MinWidth);

            width = Math.Min(width, this.MaxWidth);

            height = Math.Max(height, this.MinHeight);

            height = Math.Min(height, this.MaxHeight);

            return new Size(width, height);

        }

 

And, the Arrange pass:

        protected override Size ArrangeOverride(Size finalSize)

        {

            foreach (Item item in this.items)

            {

                item.Element.Arrange(new Rect(item.X, item.Y, item.Element.DesiredSize.Width, item.Element.DesiredSize.Height));

            }

 

            return finalSize;

        }

 

Really, it’s just straight forward how panels work…

So, there it is – a toolbox.

[Source code]

PostTypeIcon
4,747 Views

Comments

  • Custom Controls in Avalon Article...(Click here)
    A real lap around longhorn to build a toolbox control....
    April 15, 2005 8:56 AM