Follow Me on Instagram Subscribe via RSS Feed

PivotViewer Basics: Custom Item Adorners

February 3, 2012

Well we are coming to the end of the PivotViewer Basics series.  To round it out, I thought I would continue on with the last post which discussed Basic Item Adorners.  In that post we looked at implementing the custom actions that you found in the first PivotViewer.  This post will look at taking it one step further and look at what it takes to build our own item adorners. 


Basic Border Adorner

As we have done in the rest of the series, we will start with the code from the first post in this series, Client-Side Collections.  You can download that project here : PVB01_ClientSideCreation.zip.

We are going to start the first example with the basics.  Once you set your own item adorner, you will lose the very basic adorner that comes out of the box, the highlight border.  If you need to refresh your memory, run the project above and you will see that every time you mouse over or select an item it is highlighted with a light blue border around it.

If you dig into the PivotViewer XAML you will find that there is actually a bug here.  That light blue border is actually the selected border.  The mouse over border is a different color.  The item adorner defines a couple visual states in the group “ItemStates”.  The selected border is driven off of the “IsSelected” state.  However, the “IsSelected” state also fires on a simple mouse over, even if the items itself is not selected.  Since the selected border sits on top of the mouse over border, you will always see the selected one.  What does this mean for us?  It simply means we are not going to use the “IsSelected” visual state to drive our adorner.

Now that we have that out of the way, let’s create our item adorner.  If you read the last post , you might recall that the PivotViewer has a ItemAdornerStyle where we implemented the PivotViewerBasicItemAdorner.  Our first item adorner is going to look very similar, except we are going to remove the basic item adorner.  In fact, to show a point, it is going to look rather blank.

<Style x:Key="basicAdorner" TargetType="pivot:PivotViewerItemAdorner">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate>
                <Grid Background="Transparent">
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

To assign our adorner to the PivotViewer, we can set the ItemAdornerStyle property to our new style.

<pivot:PivotViewer x:Name="pViewer" 
ItemAdornerStyle="{StaticResource basicAdorner}">
   ...
</pivot:PivotViewer>

If you run the project you will see that it’s not very exciting.  In fact, you will notice that the highlight border is missing completely.  However, that’s good.  The gives us a nice blank canvas to start from.  In order to add our highlight border back, we are going to add two Border objects to our Grid.  The first one will be visible when we you mouse over an item.  The second will show when the item is selected.  By adding the selected Border second, it will hide the mouse over one when an item is selected and the mouse is over it.

<Style x:Key="basicAdorner" TargetType="pivot:PivotViewerItemAdorner">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate>
                <Grid Background="Transparent">
                    <VisualStateManager.VisualStateGroups>
                        <VisualStateGroup x:Name="CommonStates">
                            <VisualState x:Name="Normal"/>
                            <VisualState x:Name="MouseOver">
                                <Storyboard>
                                   <DoubleAnimationUsingKeyFrames 
                   Storyboard.TargetProperty="(UIElement.Opacity)"
                       Storyboard.TargetName="ButtonBorder">
                                 <EasingDoubleKeyFrame KeyTime="0" 
                                        Value="1"/>
                                 </DoubleAnimationUsingKeyFrames>
                                </Storyboard>
                            </VisualState>
                            <VisualState x:Name="Disabled"/>
                        </VisualStateGroup>
                        <VisualStateGroup x:Name="ItemStates">
                            <VisualState x:Name="Default"/>
                            <VisualState x:Name="IsSelected"/>
                        </VisualStateGroup>
                    </VisualStateManager.VisualStateGroups>
                  <conv:CustomAdorner DataContext="{Binding .}"/>
                    <Border x:Name="ButtonBorder"
                                        Margin="-1"
                                        BorderThickness="4"
                                        BorderBrush="#AAfffc08"
                                        Opacity="1"/>
                    <Border x:Name="FocusedItemBorder"
                             Visibility="{Binding IsItemSelected, 
                  RelativeSource={RelativeSource TemplatedParent},
                             Converter={StaticResource convBool}}"
                                        Margin="-1"
                                        BorderThickness="4"
                                        BorderBrush="Purple"
                                    Opacity="1"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now we have some meat to work with here.  Let’s take a closer look at what is going on.  First you will notice that we added a couple of VisualStateGroups.  For this example we are going to leave the IsSelected blank due to that bug I discussed earlier.  In the MouseOver state we are setting our ButtonBorder Border objects Opacity to 1.

So how do we show the FocusedItemBorder?  The PivotViewerItemAdorner class (whose style we are currently setting) has a IsItemSelected property.  By binding the Visibility of our Border to it, we can toggle based off of that value.  In order to convert the IsItemSelected property from a bool to a Visibility, I had to create a ValueConverter that looks like this.

*Note: This is a handy little ValueConverter and you should keep this type of converter handy for your projects.

public class BoolToVisibility : IValueConverter
{

    public object Convert(object value, 
        Type targetType, object parameter, 
        System.Globalization.CultureInfo culture)
    {
        if(value is bool)
        {
            if((bool)value)
            {
                return Visibility.Visible;
            }
        }

        return Visibility.Collapsed;
    }

    public object ConvertBack(object value, 
        Type targetType, object parameter, 
        System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Don’t forget to add the ValueConverter to our XAML so we can reference it in our adorner.

<conv:BoolToVisibility x:Key="convBool"/>

If you rerun your application, you should now have two different borders that show on your trading cards.  A yellow border on the mouse over and a purple one when you select the item.  If you were wanting nothing more to get the default borders to work correctly, you are now done.  Hopefully you want to stick around and see how to take this a bit further though. Smile

Adding Custom XAML

The above example is a great starting point, but what if we want to add a bit more interactivity in our adorners?  Turns out, it is rather easy (with a few minor catches) to do this.  We are going to wrap the rest of our adorner in a UserControl, CustomAdorner.xaml.   Our UI is going to consist of a single button for the demo’s sake.  Just to separate it from the default item adorner, we will center this button on the bottom of the card.  Here is what our new UserControl looks like:

<Grid x:Name="LayoutRoot" Background="Transparent"  >
    <StackPanel x:Name="spContainer" Orientation="Horizontal" 
       HorizontalAlignment="Center" VerticalAlignment="Bottom">
        <Button Content="Time for Change" Width="200" 
               Height="50" Click="Button_Click"/>
    </StackPanel>
</Grid>

The Button has a Click event handler.  Jumping to our code behind, we will show a simple MessageBox to prove we are getting what we expect.

private void Button_Click(object sender, RoutedEventArgs e)
{
    if(DataContext is DemoItem)
    {
        MessageBox.Show("DemoItem : " 
              + (DataContext as DemoItem).ShortName);
    }
}

With our UserControl completed, let’s add it to our item adorner. 

<Grid Background="Transparent">
    <VisualStateManager.VisualStateGroups>
...
    </VisualStateManager.VisualStateGroups>
    <conv:CustomAdorner DataContext="{Binding .}"
        Visibility="{Binding IsItemSelected, 
        RelativeSource={RelativeSource TemplatedParent}, 
        Converter={StaticResource convBool}}"/>
    <Border x:Name="ButtonBorder"
                Margin="-1"
                BorderThickness="4"
                BorderBrush="#AAfffc08"
                Opacity="1"/>
    <Border x:Name="FocusedItemBorder"
            Visibility="{Binding IsItemSelected, 
            RelativeSource={RelativeSource TemplatedParent}, 
            Converter={StaticResource convBool}}"
                Margin="-1"
                BorderThickness="4"
                BorderBrush="Purple"
            Opacity="1"/>
</Grid>

If you rerun the project, you will see that our button is now shown on the trading cards whenever you mouse over a trading card.  If you click the button, you will get a MessageBox with the name of the DemoItem displayed.  Instant success right?  Well, sort of.

image

You might notice that this isn’t exactly what we are looking for.  If you zoom into an item, the button is positioned nice and neat in the bottom center of card.  However, when zoomed in it looks something like above. 

Why is this happening?  It is important to remember that the trading cards are XAML that have been rendered to an image.  When you define a ItemTemplate, it is always important to design for the largest size that you want to display.  This will insure that your cards always look clean.

This same philosophy is carried over to the item adorners.  The Grid in our UserControl is adjusting to the current display width and height of the trading card.  However, we defined our Button to a static width and height.  Therefore, as you zoom out, the Button begins to take up too much space.  The opposite is true if you design it too small.  Zooming into the object will make the Button look out of place by being too small.

So how do we address this?  Your first thought might be to change the size of the Button as the Grid changes its size.  While this will work, what happens when you have more than one element?  What if the item isn’t on the bottom, but is some set offset from the top?  Scaling can quickly become an issue. 

Fortunately, Silverlight has already provided an answer for us. Silverlight 5 contains a control called the ViewBox, which use to be a part of the Toolkit.  The ViewBox will automatically scale the UI within it to the appropriate size while maintaining accurate spacing, etc. This means that we can scale our Grid and everything within it will scale accordingly.

In order to implement our ViewBox scenario, we first must decide a base size for our adorner.  This is the size that all of the child elements will be designed to.  For our example, let’s choose 500×500.  We are going to wrap our StackPanel inside a new Grid that is set to the 500×500.  That Grid will then be wrapped in a ViewBox.

<Grid x:Name="LayoutRoot" Background="Transparent" >
    <Viewbox>
        <Grid x:Name="Container" Background="Transparent" 
              HorizontalAlignment="Center" 
              VerticalAlignment="Center" 
              Height="500" Width="500" 
              RenderTransformOrigin="0.5,0.5">
            <StackPanel x:Name="spContainer" 
                        Orientation="Horizontal" 
                        HorizontalAlignment="Center" 
                        VerticalAlignment="Bottom">
                    <Button Content="Time for Change" 
                            Width="200" 
                            Height="50" Click="Button_Click" 
                            HorizontalAlignment="Center"
                       VerticalAlignment="Center"/>
            </StackPanel>
        </Grid>
    </Viewbox>
</Grid>

If you rerun your application, you will now see that the button scales according to the size of the trading card.

image

image

If you wanted the button to only show up when the item is selected, then we can borrow the Visibility binding for our selected Border.

<conv:CustomAdorner DataContext="{Binding .}"
                    Visibility="{Binding IsItemSelected, 
    RelativeSource={RelativeSource TemplatedParent}, 
    Converter={StaticResource convBool}}"/>

And there you have it.  The sky is now the limit on what you can do.  Well, that is until you run into a wall somewhere that I haven’t found.  Regardless, this does open up a great deal of possibilities on what you can add to your trading cards.

You can download the source to this post here : PVB05_CustomAdorner.zip

As I said at the beginning of this post, this wraps up our basics series.  What’s next?  Well I have a stack of random posts that need to be written to address various topics.  I thought I would then come back and maybe do a series on trading card designs. Until then…

Happy Pivoting…

Series Navigation<< PivotViewer Basics : Basic Item Adorners

Comments (5)

Trackback URL | Comments RSS Feed

  1. Tony says:

    Cool… Thanks very much very valuable.

  2. zahkoonkoo says:

    Many thanks, this tuto is very helpful.

  3. Chris says:

    Any thoughts on why an image won’t appear in the custom adorner? The trading card is itself an image sourced via http url. My requirement is to place an image in the lower left hand corner. I’ve tried using an Image as the single control in the adorner’s grid, an image as button content, an image as button background, etc, but no luck. When I use your demo, the image appears just fine. Thoughts?

  4. Tony Champion says:

    is the image a local image or something you are pulling remotely?

  5. chris says:

    I’m a Silverlight newbie (with some WPF experience). I was actually addressing the Source property incorrectly using an internal image resource. I used a relative path (e.g., Source=”..\my_close_button.png”), which works fine in the VS 2010 designer, but the Silverlight app couldn’t locate the image at runtime. All worked as expected once I used the correct Source syntax (as provided by the Properties panel). Thanks for the response!

Leave a Reply