Refactoring the CoolMenu Control – Part 3: CoolMenu Behaviors

Part 1: Inheriting from ItemsControl
Part 2: Building the ItemContainerGenerator

Introduction
One of the suggestions I have received from feedback on this control was to allow users of the CoolMenu to add custom effects.  There are 2 different places where I could see a user wanting to change the default behavior on this control. The first place would be how the control behaves as the user moves the mouse over each item.  The other place would be the behavior exhibited by the control when a user clicks on a particular item.  I have spent a lot of time thinking about this and I came up with a solution (but not the best, in my opinion).

Visual State Manager Dead-End
When I first set out to write this control, I wanted to place all of the behavior inside of the Visual State Manager.  Doing this would allow a user to easily re-template the control and drop in a new behavior for the various states.  After closer examination and a lot of searching, I’m beginning to think that this isn’t possible in Silverlight 2 (Please prove me wrong!!).  Suppose we defined a state for each of the the various sizes that a menu item could reside in.  This would work just fine if the destination sizes were fixed at a specific value, but the problem is that the various sizes are dynamic in each situation.  Somehow, I need to be able to declare the Visual State Manager for a CoolMenuItem like so:

<vsm:VisualStateManager.VisualStateGroups>
    <vsm:VisualStateGroup x:Name="CommonStates">
        <vsm:VisualState x:Name="Normal" />
        <vsm:VisualState x:Name="HoverSmall">
            <Storyboard>
                <DoubleAnimation
                  Storyboard.TargetName="CoolMenuItemRootElement"
                  Storyboard.TargetProperty="(UIElement.Height)"
                  Duration="0"
                  To="{MaxItemHeight*0.5}" />
                <DoubleAnimation
                  Storyboard.TargetName="CoolMenuItemRootElement"
                  Storyboard.TargetProperty="(UIElement.Width)"
                  Duration="0"
                  To="{MaxItemWidth*0.5}" />
            </Storyboard>
        </vsm:VisualState>
        <vsm:VisualState x:Name="HoverMedium">
            <Storyboard>
                <DoubleAnimation
                  Storyboard.TargetName="CoolMenuItemRootElement"
                  Storyboard.TargetProperty="(UIElement.Height)"
                  Duration="0"
                  To="{MaxItemHeight*0.75}" />
                <DoubleAnimation
                  Storyboard.TargetName="CoolMenuItemRootElement"
                  Storyboard.TargetProperty="(UIElement.Width)"
                  Duration="0"
                  To="{MaxItemWidth*0.75}" />
            </Storyboard>
            ...
            ...
            ...
        </vsm:VisualState>
    </vsm:VisualStateGroup>
</vsm:VisualStateManager.VisualStateGroups>

Maybe this is something that would work if we were able to use element-to-element data-binding in conjunction with a custom IValueConverter?  I don’t really know for sure.  If anyone knows a similar technique for accomplishing this, please let me know!  I really want to be able to do something like this!

My Approach
So, my next guess was to add another layer of abstraction.  I introduced the concept of a CoolMenuBehavior which conforms to an interface called ICoolMenuBehavior:

public interface ICoolMenuBehavior
{
    void Initialize(CoolMenu parent, CoolMenuItem element);
    void ApplyHoverBehavior(int proximity, CoolMenuItem element);
    void ApplyMouseLeaveBehavior(CoolMenuItem element);
    void ApplyMouseDownBehavior(int selectedIndex, CoolMenuItem element);
    void ApplyMouseUpBehavior(int selectedIndex, CoolMenuItem element);
}

The CoolMenu now comes prepackaged with a DefaultCoolMenuBehavior and performs all of the things that you would expect out of the box.  Since the CoolMenu polymorphically calls the methods on the DefaultCoolMenuBehavior, you could swap out the DefaultCoolMenuBehavior if you were so inclined.  Additionally, if you wanted to just override parts of the menu’s default behavior, you could simply inherit from DefaultCoolMenuBehavior and inject your own logic as needed.

As an example, I created a simple photo-gallery application and slightly enhanced the default behavior of the cool menu.  In case you are wondering, these are pictures I took of my little nephew with my Sony Alpha-700.

Click here for a Live Demo

baby_gallery

If you look carefully at the cool menu in the bottom of the image, you can see that the selected and neighboring images are slightly more opaque than the others.  I achieved this effect by inheriting from DefaultCoolMenuBehavior and inserting own my custom logic:

public override void Initialize(CoolMenu parent, CoolMenuItem element)
{
    element.Opacity = 0.4;
    base.Initialize(parent, element);
}

public override void ApplyHoverBehavior(int proximity, CoolMenuItem element)
{
    switch (proximity)
    {
        case 0:
            element.Opacity = 1;
            break;
        case 1:
            element.Opacity = 0.75;
            break;
        default:
            element.Opacity = 0.4;
            break;
    }

    base.ApplyHoverBehavior(proximity, element);
}

public override void ApplyMouseLeaveBehavior(CoolMenuItem element)
{
    element.Opacity = 0.4;
    base.ApplyMouseLeaveBehavior(element);
}

That’s not too bad, but I still think it would be better if we could do all of this in XAML.

By the way, this application was a breeze to write using the CoolMenu control.  Don’t believe me?  I’ve include the source below:

Page.xaml

<Grid x:Name="LayoutRoot" Background="Black">
    <Grid.RowDefinitions>
        <RowDefinition Height="600" />
        <RowDefinition Height="*" />
    </Grid.RowDefinitions>
    <Image Grid.Row="0" x:Name="mainImage" />
    <sc:CoolMenu Grid.Row="1" 
                 x:Name="gallery" 
                 MenuIndexChanged="CoolMenu_MenuIndexChanged" 
                 VerticalAlignment="Bottom" 
                 Margin="30">
        <sc:CoolMenu.CoolMenuBehavior>
            <loc:CoolMenuGalleryBehavior 
                MaxItemHeight="180" 
                MaxItemWidth="180" 
                BounceEnabled="False" />
        </sc:CoolMenu.CoolMenuBehavior>
        <sc:CoolMenu.ItemTemplate>
            <DataTemplate>
                <Border Grid.Row="0" BorderBrush="#FF212121" Margin="3" BorderThickness="3">
                    <Image Source="{Binding}" Margin="3"  />
                </Border>
            </DataTemplate>
        </sc:CoolMenu.ItemTemplate>
        </sc:CoolMenu>
</Grid>

Page.xaml.cs

private void Page_Loaded(object sender, RoutedEventArgs e)
{
    var images = new ObservableCollection<string>
                 {
                     "/img/DSC01605.jpg",
                     "/img/DSC01610.jpg",
                     "/img/DSC01640.jpg",
                     "/img/DSC01661.jpg",
                     "/img/DSC01663.jpg",
                     "/img/DSC01682.jpg",
                     "/img/DSC01718.jpg",
                     "/img/DSC01720.jpg"
                 };
    gallery.ItemsSource = images;
}

private void CoolMenu_MenuIndexChanged(object sender, SelectedMenuItemArgs e)
{
    mainImage.Source = new BitmapImage(new Uri(e.Item.Content.ToString(), UriKind.Relative));
}

Conclusion
So, I’m still tweaking the code and I haven’t checked this into Silverlight Contrib yet, but as soon as I do, you can be certain that I will post an update here.  Please give me your feedback and suggestions, if you think I should change something, tell me about it!  Later!


Feedback

# Silverlight Cream for February 02, 2009 -- #505

Gravatar In this issue: Boyan Mihaylov, Matthias Shapiro, Page Brooks, Dave Britton, Jesse Liberty, and Jobi Joy 2/3/2009 1:14 AM | Community Blogs

# re: Refactoring the CoolMenu Control – Part 3: CoolMenu Behaviors

Gravatar Can't you just use a Binding Converter to bind the Visual State Manager then the converter would apply the math required.
You may need more than one converter to accomplish this depending on what object you bind(could be bound to item, not just a property) and how much specific math you want to apply. 2/28/2009 6:42 PM | Steele Price

# re: Refactoring the CoolMenu Control – Part 3: CoolMenu Behaviors

Gravatar Thanks for the feedback Steele, I'll give that a shot. 2/28/2009 7:13 PM | pbrooks

Comments have been closed on this topic.