Decrease Font Size
Increase Font Size
   BLOG

Silverlight - Creating Image Map with Hotspots

by bryian 28. December 2009 16:56

Create Hot spots with Silverlight | Silvelright image Hotspots| Silverlight image hotspots| Silverlight image hot region| Silverlight image map, Silverlight pen tools

Introduction

Recently I was working on a Silverlight application and one of the pages contains a graphic. One of the requirements is that the graphic should contain clickable regions (Hotspots) and display a dynamic navigation menu when the end users click on it. An image that contains one or more Hotspots or clickable area is called an image map. I have put together a simple tutorial on how to get this done.

Getting Started

Image map can be created by using Microsoft Expression Design or Expression Blend.

  1. If you do not have Microsoft Expression Design, you can download a trial version from here.
  2. If you do not have Microsoft Expression Blend, you can download a trial version from here.

Microsoft Expression Designer

Prepare your favorite image. In this tutorial, I used a map graphic to demonstrate that we can create Hotspots in different shapes with Microsoft Expression Designer/ Blend. I downloaded a random map from here. Follow the step from below.

  1. Open Microsoft Expression Design 3
  2. Go to File, New, Fill up all the information and click the OK button.
  3. Go to View, check Snap to Points and Snap to Pixels.
  4. File, Import Image, and browse for your image and click the Open button.
  5. Double click on the Layer1 and rename it to MainImage.

At this point, you should see something like below.

Figure 1
Main Image

Add a new layer at the top of MainImage layer, highlight it and select the Pen tool and draw a hotspot around Russia.

Figure 2

New layer
Pen Tool Russia

Double click the Layer 2 and name it Russia or something meaningful so we can utilize it in the Silverlight application. Highlight the points and click on the properties tab. See below.

Figure 3

Add property

Pick a color you like and set it Opacity to 40%.

Figure 4

Set opacity

After you have finished drawing the Hotspots, click on File, Export and copy the setting from below.

Figure 5

Export

Putting everything together

Open Visual Studio 2008, go to File, New, Project, and choose the Silverlight Application template. If you don't have the Silverlight Controls Toolkit, you can download it from here. Make sure you have added the reference for System.Windows.Controls.Toolkit. Open the MainPage.xaml and drag a Viewbox control on to the page. This control will stretch the map image inside it proportionally when we resize the browser. Then copy the content in the MapsHotSpot.xaml into the Viewbox content in the MainPage.xaml. See below.

Listing 1

<UserControl x:Class="MapsHotspotDemo.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:controlsToolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    mc:Ignorable="d" d:DesignWidth="640" d:DesignHeight="480"> 
  <Grid x:Name="LayoutRoot">
        <controlsToolkit:Viewbox  x:Name="MapsViewbox"  >
<!-- Maps content here -->
               <Canvas xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
                    x:Name="MapsHotSpot" Width="800" 
                    Height="600" Clip="F1 M 0,0L 800,0L 800,600L 0,600L 0,0" 
                    UseLayoutRounding="False">
			...
		   </Canvas>
<!-- End Maps content here -->
        </controlsToolkit:Viewbox>
  </Grid>
</UserControl>

Expression Blend

If you choose to do it by using Expression Blend, here are the steps to follow.

  1. Open Microsoft Expression Blend.
  2. Click on File, select New Project and follow the setup from below.

Figure 6

Blend New Project

Add a Viewbox into the LayoutRoot and name it MapsViewbox, then add a Canvas inside the Viewbox and name it MapsHotspot. After that, add a Canvas into MapsHotspot and an Image control into the Canvas. From the Image control properties, specify the image source and setup the appropriate width and height of the image. Then, create another Canvas and name it Russia or something meaningful or unique. Make sure that the width and height of the newly created Canvas match the image in order to draw points on it. Click on the Pen tool and draw points or vertices around Russia. Pick a brush color and set it Opacity to certain percentages. Repeat the same procedure for the rest of the countries.

Figure 7

Blend Hot Spot

Tooltips

Remember that we named each layer at the beginning. The layers and points are translated into Canvas and Path objects respectively when we export them into XAML. We can provide each layer with a unique name or include a tag attribute to it and use it to pull the country information from a database or resource dictionaries. For simplification sake, we will have a method to loop through each child elements in the MapsHotSpot Canvas, grab their name and set it to the tooltip content. Attach a MouseMove event to each layer to highlight the country on mouse hover. Also, attach a MouseLeftButtonUp event to pop up a menu when the left mouse button is released. See Listing 2.

Listing 2

foreach (Canvas c in (this.FindName("MapsHotSpot") as Canvas).Children)
            {
                if (!string.IsNullOrEmpty(c.Name))
                {
                    c.Cursor = Cursors.Hand;
                    ToolTip toolTip = new ToolTip { Content = c.Name };
                    c.SetValue(ToolTipService.ToolTipProperty, toolTip);

	c.MouseMove += new MouseEventHandler(c_MouseMove);
                    c.MouseLeftButtonUp += new 			 	MouseButtonEventHandler(c_MouseLeftButtonUp);
                }
            }

Displayed below is the implementation of the c_MouseMove method. This method will highlight the Canvas/ country and clear the previous selection, if there is any, during mouse hover event.

Listing 3

void c_MouseMove(object sender, MouseEventArgs e)
        {
            Canvas c = sender as Canvas;
            ResetLastSelected();

            if (!string.IsNullOrEmpty(c.Name))
            {
                lastSelected = c.Name;
                SetCanvasColor(c, Color.FromArgb(255, 92, 112, 171), 2, Colors.Green);
            }
        }

View in browser, you should see something like below. If you resize the browser, you will still see the full map. Place the mouse over the map image to see the tooltip and the highlighted region.

Figure 8

Preview

We will build a simple menu with several links in it. First, add a simple class to hold the link properties. See below for an example.

Listing 4

public class Links
    {
        public string Title { get; set; }
        public string URL { get; set; }

        public Links(string t, string u)
        {
            Title = t;
            URL = u;
        }
    }

After that, create a global List (T) class to hold the link objects and populates the link class with the below data on page load.

Listing 5

private List<Links> _links = new List<Links>();

void SetupLinks()
        {
            _links.Add(new Links("About the people", "http://www.countryreports.org/{0}.aspx"));
            _links.Add(new Links("Economy", "http://www.economicexpert.com/a/{0}.htm"));
            _links.Add(new Links("Global Statistics", "http://www.geohive.com/cntry/{0}.aspx"));
            _links.Add(new Links("Population", "http://www.geohive.com/charts/population1.aspx"));
            _links.Add(new Links("Wiki", "http://en.wikipedia.org/wiki/{0}"));
        }

Below is the implementation of the c_MouseLeftButtonUp method. This method will bring up the popup menu by calling the PopulateContextMenu method in response to the mouse left button click event. The PopulateContextMenu take a country name as an argument, loop through the list of link objects and concatenate the country name to it. The purpose of the PositionContextMenu method is to set the popup menu position. I also added animation to fade in and fade out the popup menu, please refer to the MainPage.xaml.

Listing 6

void c_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            Canvas c = sender as Canvas;
            if (!string.IsNullOrEmpty(c.Name))
            {
                PopulateContextMenu(c.Name);
                PositionContextMenu(e.GetPosition(contextMenu.Parent as UIElement), true);
                e.Handled = true;
            }
        }

        void PositionContextMenu(Point p, bool useTransition)
        {
            if (useTransition)
                contextMenu.IsOpen = false;
            contextMenu.HorizontalOffset = p.X;
            contextMenu.VerticalOffset = p.Y;
            contextMenu.IsOpen = true;
        }

void PopulateContextMenu(string country)
        {
            contextListBox.Items.Clear();

            foreach (Links l in _links)
            {
                HyperlinkButton hlb = new HyperlinkButton();
                hlb.Content = l.Title;
                hlb.NavigateUri = new Uri(string.Format(l.URL, country));
                hlb.TargetName = "_blank";

                contextListBox.Items.Add(hlb);
            }   
        }

At this point, you should see something like below.

Figure 9

Mouse click map

The final piece is to hide the popup menu and clear the highlighted country once we click on the non- clickable area of the map image. In order to achieve that, we can attach a MouseLeftButtonUp event to the Layout grid on page load. See below.

Listing 7

LayoutRoot.MouseLeftButtonUp += new MouseButtonEventHandler(LayoutRoot_MouseLeftButtonUp);

void LayoutRoot_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {      
            ResetLastSelected();
            HideMenu();
        }

        void HideMenu()
        {
            HidePopup.Begin();
            contextMenu.HorizontalOffset = -50.0;
        }

Points of Interest

Initially I was planning to use the right mouse button click event class that I found from here to trigger the popup menu, but I decided to leave it out after reading an article from here about its disadvantages.

I found this article useful although it was written in VB.NET and I can't get it to compile, but I'm able to utilize the logic embedded in it.

Conclusion

If you find any bugs or disagree with the contents, please drop me a line and I'll work with you to correct it. I would suggest downloading the demo and explore it in order to grasp the full concept of it because I might left out some useful information. I hope someone will find this article useful on how to create image Hotspots or map on any images.

Resources

Adding right click to Silverlight
Limitations of Windowless mode for Silverlight
Silverlight - World Map

Watch this script in action

Demo

Downloads

Download

ASP NET Horizontal Menu Control

by bryian 14. December 2009 18:18

ASP.NET menu control safari|

Problem displaying asp .net menu control in Google Chrome|

Breadcrumb (Sitemappath) Control|

ASP.NET Menu control|

Displaying Menu Control Horizontal With SubMenus|

ASP.NET Menu AccessKey|

ASP.NET Menu Target

Last Updated - 02/26/2010

  • Removed PathSeparator of SiteMapPath before the root node as suggested by The Code Project member, kentex2000.
  • Added SiteMap.CurrentNode != null to SiteMap_SiteMapResolve method.

Last Updated - 02/03/2010

Introduction

A few weeks ago, I was working on an ASP.NET web application and need a simple horizontal menu with submenu. I decided to use ASP.NET Menu control, just drag and drop the control on to the page. Simple enough, but the control does not provide access key and target window support on menu item. I have put together a tutorial on how to:

  1. Include an access key attribute
  2. Include a target attribute
  3. Include a Site Map Path


Figure 1
Sample results

 

Getting Started

Here is the structure of my project. You are welcome to download this demo.

Figure 2
Project Structure

Putting everything together

First, add a Site Map to the website project. Open the web.sitemap file and populate it with your navigation data and structures. To underline certain character of the menu title, we can use the HTML underline tag (<u></u>). In order to parse the XML flawlessly we must replace the less than sign (<) with & lt; (no spaces). Then, include an accesskey and target attribute with a value to each siteMapNode. See below for example.

Site Map

Listing 1

<siteMapNode>
      <siteMapNode url="Default.aspx" title="& lt;u>H& lt;/u>ome" description="Home" 
                   accesskey="H" />
      <siteMapNode url="~/Views/Menu1.aspx" title="<u>M</u>enu1"  
                   description="Menu1" accesskey="M" />
        <siteMapNode url="~/Views/Menu2.aspx" title="M<u>e</u>nu2" 
                     description="Menu2" accesskey="E" />
    
    <siteMapNode url="~/Views/Menu3.aspx" title="Me<u>n</u>u3" 
                 description="Menu3" accesskey="N" target="_blank" />
        
    <siteMapNode url="~/Views/Menu4.aspx" title="Men<u>u</u>4" 
                 description="Menu4" accesskey="U">
      <siteMapNode url="~/Views/Menu4Sub1.aspx" title="Menu4<u>S</u>ub1" 
                   description="Menu4Sub1" 
                   accesskey="S" />
      <siteMapNode url="~/Views/Menu4Sub2.aspx" title="Menu4Su<u>b</u>2" 
                   description="Menu4Sub2" 
                   target="_blank" accesskey="B" />
    </siteMapNode>
……
….
  </siteMapNode>
</siteMap>

Master Page

Add a Master Page to the website project. Drag a SiteMapDataSource control on to the page and then the menu control and wrap the menu control inside a div tag. The details description of each menu property can be found on Menu Properties Menu Properties. Set the staticdisplaylevels ="2" and orientation="Horizontal" to display the menu control in horizontal mode. We can use an inline style sheets or place the CSS style in an external file. In this tutorial, the CSS style is located in style.css file. See listing 2.

Listing 2

<asp:SiteMapDataSource id="MenuSource" runat="server" />
<div class="background">
  <asp:menu id="NavigationMenu" CssClass="NavigationMenu"  
        staticdisplaylevels="2" DynamicHorizontalOffset="1"
        staticsubmenuindent="1px" MaximumDynamicDisplayLevels="4"
        orientation="Horizontal"   
        DynamicPopOutImageUrl="~/Images/right-arrow.gif" 
        StaticPopOutImageUrl="~/Images/drop-arrow.gif"
        datasourceid="MenuSource"    
        runat="server" Height="30px">

        <staticmenuitemstyle ItemSpacing="10" CssClass="staticMenuItemStyle"/>
        <statichoverstyle CssClass="staticHoverStyle" />
       <StaticSelectedStyle CssClass="staticMenuItemSelectedStyle"/> 
        <DynamicMenuItemStyle CssClass="dynamicMenuItemStyle" />      
        <dynamichoverstyle CssClass="menuItemMouseOver" />
        <DynamicMenuStyle CssClass="menuItem" />
       <DynamicSelectedStyle CssClass="menuItemSelected" />
     
       <DataBindings>        
             <asp:MenuItemBinding DataMember="siteMapNode" 
                    NavigateUrlField="url" TextField="title"  
                    ToolTipField="description" />
        </DataBindings>

      </asp:menu>
</div>

Drag a SiteMapPath control on to the page. The purpose of this control is to display navigation path that shows the user the current page location. See listing 3.

Listing 3

<div id="e">
       <asp:SiteMapPath ID="SiteMapPath1" runat="server" 
                RenderCurrentNodeAsLink="true" 
                CssClass="currentNodeStyle"
            PathSeparator=" >> ">
            <PathSeparatorStyle ForeColor="#5D7B9D" CssClass="currentNodeStyle" />
            <CurrentNodeStyle ForeColor="#333333" CssClass="currentNodeStyle" />
            <NodeStyle ForeColor="#7C6F57"  CssClass="currentNodeStyle"  />
            <RootNodeStyle  ForeColor="#5D7B9D" CssClass="currentNodeStyle"  />
    </asp:SiteMapPath> 
</div>  

Master Page code behind

In the Master Page code behind, include a MenuItemDataBound and SiteMapResolve event handlers on to the Page_Load event. The purpose of the former event is to insert the target attribute value and create access key for the menu item before it is rendered or displayed in a Menu control. The latter event is to modify the text displayed by the SiteMapPath control.

Listing 4

NavigationMenu.MenuItemDataBound += new MenuEventHandler(NavigationMenu_MenuItemDataBound);
SiteMap.SiteMapResolve += new SiteMapResolveEventHandler(SiteMap_SiteMapResolve);

Below is the implementation of the NavigationMenu_MenuItemDataBound method. The MenuItemDataBound event occurs when a menu item in a Menu control is bound to data. That being said, it will loop through each siteMapNode and look for the accesskey and target attribute. There is a target property associated with the menu item and we can set its target window with the target attribute value. See listing 5.

Listing 5

void NavigationMenu_MenuItemDataBound(object sender, MenuEventArgs e)
    {
        SiteMapNode node = (SiteMapNode)e.Item.DataItem;
       
        //set the target of the navigation menu item (blank, self, etc...)
        if (node["target"] != null)
        {
            e.Item.Target = node["target"];
        }
        //create access key button
        if (node["accesskey"] != null)
        {
            CreateAccessKeyButton(node["accesskey"] as string, node.Url);
        }
    }

To get the access key to work, add a Panel control on to the master page and a JavaScript function to redirect the webpage to the one that is specified. See below.

Listing 6

<asp:Panel ID="AccessKeyPanel" runat="server" />
<script type="text/javascript">
 function navigateTo(url) {
    window.location = url;
 }
</script>

Below is the implementation of the CreateAccessKeyButton method. Create an HtmlButton control dynamically and attach an onclick event to it. Set the style.left property to -2555px to hide the control. A complete list of access key in different browsers is available here.

Listing 7

//create access key button
    void CreateAccessKeyButton(string ak, string url)
    {
        HtmlButton inputBtn = new HtmlButton();
        inputBtn.Style.Add("width", "1px");
        inputBtn.Style.Add("height", "1px");
        inputBtn.Style.Add("position", "absolute");
        inputBtn.Style.Add("left", "-2555px");
        inputBtn.Style.Add("z-index", "-1");
        inputBtn.Attributes.Add("type", "button");
        inputBtn.Attributes.Add("value", "");
        inputBtn.Attributes.Add("accesskey", ak);
        inputBtn.Attributes.Add("onclick", "navigateTo('" + url + "');");

        AccessKeyPanel.Controls.Add(inputBtn);
    }

The SiteMap.SiteMapResolve event get trigger when the CurrentNode property is accessed. It will call the ReplaceNodeText method recursively and replace the HTML underline tag. See listing 8.

Listing 8

SiteMapNode SiteMap_SiteMapResolve(object sender, SiteMapResolveEventArgs e)
    {
        if (SiteMap.CurrentNode != null)
        {
            SiteMapNode currentNode = SiteMap.CurrentNode.Clone(true);
            SiteMapNode tempNode = currentNode;
            tempNode = ReplaceNodeText(tempNode);

            return currentNode;
        }
        return null;
    }

    //remove <u></u> tag recursively
    internal SiteMapNode ReplaceNodeText(SiteMapNode smn)
    {
        //current node
        if (smn != null && smn.Title.Contains("<u>"))
        {
            smn.Title = smn.Title.Replace("<u>", "").Replace("</u>", "");
        }

        //parent node
        if (smn.ParentNode != null)
        {
            if (smn.ParentNode.Title.Contains("<u>"))
            {
                SiteMapNode gpn = smn.ParentNode;
                smn.ParentNode.Title = smn.ParentNode.Title.Replace("<u>", "").Replace("</u>", "");
                smn = ReplaceNodeText(gpn);
            }
        }
        return smn;
    }

Using the Code

Since the menu is in the master page, right click the website project, add new item, Web Form and check the Select Master Page checkbox.

Points of Interest

The hover menu appears to not working on mobile devices. To remedy this problem, I include a TreeView control and set its visible property to false. This control expands its entire node by default. That will take care of the mentioned problem. In the code behind, hide the Menu control and show the TreeView control if requesting browser is a mobile device. See listing 9.

Listing 9

protected void Page_Load(object sender, EventArgs e)
    {
	if (Request.Browser.IsMobileDevice)
        {
            NavigationMenu.Visible = false;
            NavigationTreeView.Visible = true;
        }
}

When I tested the menu on IE 8, the hover menu did not render correctly. To overcome this problem, I set the DynamicMenuStyle z-index to 200, see style.css. The submenu is also not working with Google Chrome. After some research, I found the solution for it. See listing 10.

Listing 10

protected void Page_Load(object sender, EventArgs e)
    {
	if (Request.UserAgent.IndexOf("AppleWebKit") > 0)
        {
            Request.Browser.Adapters.Clear();
            NavigationMenu.DynamicMenuStyle.Width = Unit.Pixel(120);
        }
}

New Update

I have received several complaints from the reader concerning the menu control not displaying correctly on Safari and Google Chrome browsers. Somehow the menu items are stacked on each other and the submenu widths are gapped apart. After doing some research, I found the answer here, see listing 11. To fix the submenu width, remove the display:block from the dynamicMenuItemStyle in the css file.

Listing 11

protected override void AddedControl(Control control, int index)
    {
        if (Request.ServerVariables["http_user_agent"].IndexOf("Safari", StringComparison.CurrentCultureIgnoreCase) != -1)
            this.Page.ClientTarget = "uplevel";

        base.AddedControl(control, index);
    }

I also rewrite the logic to detect mobiles browser with the code from Vincent Van Zyl. See listing 12.

Listing 12

    public static readonly string[] mobiles =
          new[]
                {
                    "midp", "j2me", "avant", "docomo", 
                    "novarra", "palmos", "palmsource", 
                    "240x320", "opwv", "chtml",
                    "pda", "windows ce", "mmp/", 
                    "blackberry", "mib/", "symbian", 
                    "wireless", "nokia", "hand", "mobi",
                    "phone", "cdm", "up.b", "audio", 
                    "SIE-", "SEC-", "samsung", "HTC", 
                    "mot-", "mitsu", "sagem", "sony"
                    , "alcatel", "lg", "eric", "vx", 
                    "NEC", "philips", "mmm", "xx", 
                    "panasonic", "sharp", "wap", "sch",
                    "rover", "pocket", "benq", "java", 
                    "pt", "pg", "vox", "amoi", 
                    "bird", "compal", "kg", "voda",
                    "sany", "kdd", "dbt", "sendo", 
                    "sgh", "gradi", "jb", "dddi", 
                    "moto", "iphone"
                };

    public static bool isMobileBrowser()
    {
        //GETS THE CURRENT USER CONTEXT
        HttpContext context = HttpContext.Current;

        //FIRST TRY BUILT IN ASP.NT CHECK
        if (context.Request.Browser.IsMobileDevice)
        {
            return true;
        }
        //THEN TRY CHECKING FOR THE HTTP_X_WAP_PROFILE HEADER
        if (context.Request.ServerVariables["HTTP_X_WAP_PROFILE"] != null)
        {
            return true;
        }
        //THEN TRY CHECKING THAT HTTP_ACCEPT EXISTS AND CONTAINS WAP
        if (context.Request.ServerVariables["HTTP_ACCEPT"] != null &&
            context.Request.ServerVariables["HTTP_ACCEPT"].ToLower().Contains("wap"))
        {
            return true;
        }
        //AND FINALLY CHECK THE HTTP_USER_AGENT 
        //HEADER VARIABLE FOR ANY ONE OF THE FOLLOWING
        if (context.Request.ServerVariables["HTTP_USER_AGENT"] != null)
        {
            for (int i = 0; i < mobiles.Length; i++)
            {
                if (context.Request.ServerVariables["HTTP_USER_AGENT"].
                                                    ToLower().Contains(mobiles[i].ToLower()))
                {
                    return true;
                }
            }
        }

        return false;
    }

Add the code shown below in the Page_Load event.

Listing 13

if (isMobileBrowser())
        {
            NavigationMenu.Visible = false;
            NavigationTreeView.Visible = true;
        }

Conclusion

If you find any bugs or disagree with the contents, please drop me a line and I'll work with you to correct it.

Tested on IE 6.0/7.0/8.0, Google Chrome, Firefox, Safari

IE, Firefox, Google Chrome, Safari

History

  • 02/03/2010 - Fixed the Menu display problem in Safari and Google Chrome browsers, added new logic to detect mobiles browser

Resources

Access in different Browsers
ASP.NET Menu Control Cannot Work in Google Chrome browser and Apple Safari browser
Detecting a mobile browser in ASP.NET
IE8 and the ASP.NET Menu control
Menu Properties
Menu..::.MenuItemDataBound Event
Problem displaying asp .net menu control in Google Chrome
SiteMap..::.SiteMapResolve Event

Watch this script in action

Demo

Downloads

Download