Friday, September 12, 2008

Silverlight Tutorial: Creating a data centric web app with DataGrid, LINQ, and WCF Web Service

You can see a
working example of the finished data centric Silverlight 2.0 web application
here
. A screen shot is provided below.





Introduction




This article is a step-by-step tutorial on how to make a data centric Silverlight 2.0 web application. This tutorial you will have practical hands-on experience with all of the following technologies:






  • Silverlight 2.0 Datagrid



  • LINQ



  • Web Services




If you go through this tutorial I’ll think you’ll be amazed at how simple, powerful, and elegant these three technologies are. I’ve really started to feel that Microsoft is on a roll with some of these new technologies.




I also give you instructions on how to deploy this application to a remote web server which in my experience is probably the most difficult part. The instructions I give specifically apply to my host provider discountasp.net, however, no matter what your hosting provider you’ll probably need to do something similar.




Tutorial Part 1: Building the application and testing it locally






  1. For this tutorial you will need access to a web accessible database. As mentioned in the introduction, I use web hosting account from discountasp.net with a SQL Server 2008 add-on, but there are many options of hosting providers to choose from. To establish a connection with your remote database, select View from the menu, and then Server Explorer. Click on the connect to database option and then fill in with SQL Server Name and Database Login that you can get from your hosting provider. Click test connection to confirm that you’re set up.

















  1. Create a new SilverlightApplication called SilverlightClient. Call the solution WcfSqlDemo.









  1. VS2008 will ask you if you want to add a Web to the solution. Select “Web Application Project” instead of the default “Web Site”. Name the web WcfSqlDemoWeb.






  1. Right click on WcfSqlDemoWeb and select add, new item. Select Data, LINQ to SQL Classes, and leave the name DataClasses1.dbml




What you should see next is:






  1. If you set up your server at the beginning of the project you will already have a database in the server explorer view as I do (in my case it’s named sql2k801.SQL2008_540970). If you haven’t, click on the cylinder with a plus sign icon under Server Explorer to connect to database.






  1. Right click “Table” on your connected database and select add new table. Fill in table as shown below. Under properties name the table DemoTable.













  1. Drag DataTable onto DataClasses1.dbml






  1. Set key as the primary key







  1. View properties for DataClasses1.dml. Set the serialization mode to unidirectional so that it is compatible with web services.








  1. Okay, that takes care of your database. Now let’s set up the web service. Right click on the WcfSqlDemoWeb project and add a new item. Select the Silverlight-enabled WCF Service template and name it DemoService.svc







  1. Delete DoWork method and replace with the two methods below plus the following using references. By the way, the “var selected_rows = from rows in db.DemoTables select rows” stuff, that’s LINQ. It’s fantastic. It’s a very clean sensible query language that is built into .NET 3.5 that can be used for querying databases, XML, and even collections. One of the easier ways to figure out what link is, is to look at some examples. I recommend Microsoft’s 101 LINQ Samples . The LINQ line that I use in the GetRows method loosely means “Take the table db.DemoTable and assign the elements of that table to the collection rows then select all those rows and assign them to the collection selected rows”.




using System.Collections.Generic;
using System.Text;




and also




// Return the list of valid data
[OperationContract]
public List<demotable> GetRows()
{
DataClasses1DataContext db = new DataClasses1DataContext();
var selected_rows = from rows in db.DemoTables select rows;
return selected_rows.ToList();
}

// Insert a new data into the database
[OperationContract]
public void InsertData(string testItem1,
string testItem2)
{
DataClasses1DataContext db = new DataClasses1DataContext();

// Create a new row object.
DemoTable row = new DemoTable
{
Key = Guid.NewGuid(),
TestItem1 = testItem1,
TestItem2 = testItem2
};

// Add the new object to the collection.
db.DemoTables.InsertOnSubmit(row);

// Submit the change to the database.
db.SubmitChanges();
}



  1. Now we need to let our Silverlight client know about the web service we’ve just created. Right click SilverlightClient and select Add Service Reference. Click Discover. VS2008 should find the service from the project. Allow it to be saved in the namespace ServiceReference1.





  1. Open page.xaml in ExpressionBlend. Make a form that looks something like what I’ve created below. Name the textboxesTestItem1TxtBox and TestItem2TxtBox







  1. Now let’s add the datagrid into which the data we retrieve from the web service will be placed. The Datagrid control is not a default control of your Silverlight project, so you’ll have to add it. Right click on references in SilverlightClient and add a reference to System.Windows.Control.Data. Go to page.xaml. Add the following attribute to the user control element so we can get access to the DataGrid



        xmlns:my="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Data"





  1. Now you can go to the asset library and find DataGrid under Custom Controls.







  1. Add a DataGrid to the project and size it. Name the grid ResultsGrid. If you look at the XAML it should look similar to the below.




<my:DataGrid Margin="8,283,51,87" AutoGenerateColumns="True"
x:Name="ResultsGrid"/>









  1. Add a button and label it “Get”. The end result should be something like as follows:



  1. Add OnGetBtnClicked and OnSubmitBtnClicked to the clicked events of the respective buttons in Expression Blend. This will add the attributes to the XAML button tags and will wake up VS2008 and add the specified methods to the code behind file.

  2. Fill in the OnSubmitBtnClick and OnGetBtnClick as shown below. Create a call back to handle the GetRowsCompleted event. (By the way, are you noticing how easy this is? Look at how much is getting done with a few lines of code and look how clean and sensible these few lines are.)


private void OnGetBtnClick(object sender, RoutedEventArgs e)
{

DemoServiceClient webService = new DemoServiceClient();

webService.GetRowsCompleted +=
new EventHandler<getrowscompletedeventargs>(webService_GetRowsCompleted);

webService.GetRowsAsync();
}

void webService_GetRowsCompleted(object sender, GetRowsCompletedEventArgs e)
{

ResultsGrid.ItemsSource = e.Result;
}

private void OnSubmitBtnClick(object sender, RoutedEventArgs e)

{
DemoServiceClient webService = new DemoServiceClient();
webService.InsertDataAsync(TestItem1TxtBox.Text, TestItem2TxtBox.Text);
}


  1. Build and test. Try submitting a few items and then getting the results. You should see something like the following:


  1. Easy right? Notice how we didn’t even have to do anything to get the DataGrid to display our results other than assign our results to the DataGrid’s ItemsSource. The only down side to this simplicity is that we’re seeing everything returned including the guid representing the key. That’s not very user friendly. Let’s get rid of the auto-generation of columns and create custom ones. Also it seems I needed to add an explicit size to the DataGrid or I’d get a funny rendering of the grid when it’s empty.




<my:datagrid margin="8,283,51,85" autogeneratecolumns="False" x:name="ResultsGrid"
width="641" height="232">
<my:DataGrid.Columns>
<my:DataGridTextColumn
Header="Test Item 1"
DisplayMemberBinding="{Binding TestItem1}"/>
<my:DataGridTextColumn
Header="Test Item 2"
DisplayMemberBinding="{Binding TestItem2}"/>
</my:DataGrid.Columns>
</my:datagrid>






  1. Build and debug. The results should look exactly like before, except this time no column for Key.



  2. By now you’re probably collecting some garbage in your database, because we have no way to delete any items. Let’s change that now. First let’s modify our web service to be able to delete items from our database. Add the following to DemoService. Note that we’re using more LINQ. The line below written in LINQ loosely means “Take the table db.DemoTable and assign the elements of that table to the collection rows then select those rows where the key is the passed guid and assign those selected rows to the collection selectedrow”.




// Delete the item specified by the passed key
[OperationContract]
public void DeleteRow(Guid key)
{
DataClasses1DataContext db = new DataClasses1DataContext();
var selected_row = from rows in db.DemoTables where rows.Key==key select rows;

// Delete the selected "rows". There will actual be only one
// item in this collection because the Guid is unique and is the
// primary key for the table.
db.DemoTables.DeleteAllOnSubmit(selected_row);

// Submit the change to the database.
db.SubmitChanges();

}





  1. Add a delete button. When the delete button is clicked will delete whatever item in our DataGrid that was selected.




private void OnDeleteClick(object sender, RoutedEventArgs e)
{

DemoTable selectedRow = ResultsGrid.SelectedItem as DemoTable;

// Now access the service to delete the item
DemoServiceClient webService = new DemoServiceClient();
webService.DeleteRowAsync(selectedRow.Key);

// Now refresh the grid
webService.GetRowsCompleted +=
new EventHandler<getrowscompletedeventargs>(webService_GetRowsCompleted);

webService.GetRowsAsync();
}





  1. Rebuild and Debug. Now you can select items and delete them.


Tutorial Part 2: Deploying the web app to a remote server



  1. Publishing the application to the web can be a little difficult. The instructions below have been tried out on my web hosting provider discountasp.net, but you’re likely to need to do something similar for other host providers as well.

  2. To see the difficulty, publish the website. Now DemoService is located at your server right? All you might think you need to do is reconfigure your silverlight client to reference the service at your website. Go ahead and try it. Right click on ServiceReference1 and select Configure service reference. Add the address of the service. In my case it is http://uiprototype.web702.discountasp.net/DemoService.svc. You’ll get the error below and you won’t be able to proceed.




  1. Here’s the work around. We’re going to override the way the server creates the Service Host. Add the following class to DemoService.svc.cs



public class MyServiceHostFactory : ServiceHostFactory
{
protected override ServiceHost CreateServiceHost(Type serviceType, Uri[] baseAddresses)
{
// Specify the exact URL of your web service
Uri webServiceAddress = new Uri("http://uiprototype.web702.discountasp.net/DemoService.svc");

ServiceHost webServiceHost = new ServiceHost(serviceType, webServiceAddress);
return webServiceHost;
}
}




  1. Go to DemoService.svc, right click to view markup and you’ll see the below:



 <%@ ServiceHost Language="C#" Debug="true" Service="WcfSqlDemoWeb.DemoService" CodeBehind="DemoService.svc.cs" %>



Add the follow attribute:



Factory="WcfSqlDemoWeb.MyServiceHostFactory"



  1. Now rebuild all and publish. Now go back to ServiceReference1 Right click on ServiceReference1 and select Configure service reference. Add the address of the service. In my case it is http://uiprototype.web702.discountasp.net/DemoService.svc. Now it should accept it.


Tutorial Part 3: Configuring your application and your remote server so that you can debug in a variety of local and remote configurations.



  1. The only problem with the work around described in Part2 is that you can’t debug your application locally anymore. If you now try to debug your silverlight page and you click on either of your buttons (thereby attempting to access the webservice) you’ll get an exception saying the webservice wasn’t found. This is due to crossdomain security restrictions. When you debug, your client is hosted on localhost and web service is hosted on your remote server (in my case as discountasp.net). When the localhost hosted Silverlight client reaches out and trys to talk to the DiscountAsp.net hosted webservice, that’s a crossdomain communication and you need to explicitly grant permission for that type of access of your web service. To grant this crossdomain access, you’ll need to put a crossdomain.xml and clientaccesspolicy.xml on your remote server.



Here’s the crossdomain.xml file you’ll need to add to your web project:



<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*">
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>




Here’s the clientaccesspolicy.xml file you’ll need to add to your web project:




<?xml version="1.0" encoding="utf-8"?>
<access-policy>
<cross-domain-access>
<policy>
<allow-from http-request-headers="*" >
<domain uri="*"/>
</allow-from>
<grant-to>
<resource path="/" include-subpaths="true"/>
</grant-to>
</policy>
</cross-domain-access>
</access-policy>





  1. What if you want to go back to debugging both client and the webservice itself from localhost like we’re doing at the beginning of this tutorial. There may be a better way, but here’s what I did. Go to your Web.config file and find your appSettings tag. Replace it with the following:




<appSettings>
<!--<add key="ServiceUri_WcfSqlDemo" value="http://uiprototype.web702.discountasp.net/DemoService.svc"/>-->
<add key="ServiceUri_WcfSqlDemo" value="http://localhost:49797/DemoService.svc"/>
</appSettings>





  1. Now go back to DemoService.svc.cs



Add the following using statement:



using System.Configuration;

Change the Uri as follows in your CreateServiceHost method to look up the address in the Web.config file:


Uri webServiceAddress = new Uri(ConfigurationManager.AppSettings["ServiceUri_WcfSqlDemo"]);



  1. Right click on ServiceReference1 and click on configure service reference. Set the address to http://localhost:49797/DemoService.svc

  2. Now rebuild all and debug. Things should be working as before except now the webservice is back to being hosted locally. If you don’t believe it, go and delete everything from the server.

  3. Now you can go back and for by changing the address in ServiceReference1 and Web.config

  4. That’s it!

Thursday, August 21, 2008

Silverlight Tutorial: Skinnable Custom Control A-Z

Download SkinnableNavbar_Full_Source_--_skinnable_control_demo.zip - 1.61 MB



Below is a screen shot of the skinnable custom control that you will be able to make by the end of this tutorial. The top image is of the generic control. The bottom image is the same control with a skin applied. The control is an animated navigation bar. You can see a working example of the animated navigation bar by following this link.






Introduction



This article is a step-by-step tutorial on how to use Silverlight 2.0 to make a skinnable custom control – specifically an animated navigation bar similar to the one at the top of the silverlight.net website. By the end of this tutorial you will great practical hands-on experience in all of the following:




  • Visual state manager


  • Custom controls


  • Skinning (a way to change the look of a control without changing the underlying code)


  • Dependency properties (a way of adding custom attributes to your custom control)



This article is divided into three parts. In part one, we create a fully functioning animated navigation bar that from a user’s perspective is identical to the final control we’re going to make. If you’re only making one control, you might just stop after part one. However, if you want to take that control and package it up in a way that is easily reusable, then you’ll want to go on to part 2 of the tutorial where I show you how to turn the navigation bar into a “skinnable” custom control. “Skinnable” means that you can change the look of the control without changing the underlying code, and that brings me to part three of the tutorial. In part three, I demonstrate skinning of the newly developed custom control.



Note: The project in this tutorial used Silverlight 2 beta 2, Visual Studio 2008, and the June 2008 Preview of Expression Blend 2.5.



Tutorial Part 1: Creating a Navigation Bar Using the Visual State Manager




  1. In VS2008, select new project, empty web site. Name the website SkinnableNavbar. Note although we’re calling this project “skinnable”, what we’re going to make in the first part of the tutorial is just a regular user control. We’re doing it this way so that we can use Expression Blend to get all the visual and behavioral aspects of our design right before we go the extra step of turning it into a generic control. This will also give us a chance to introduce the VisualStateManager.


  2. Right Click on the solution, click add project and add a Silverlight Application named SkinnableNavbarXap, leave checked add test page


  3. Click on page.xaml in SkinnableNavbarXap and open in Expression Blend 2.5.


  4. The first step is to create all the visual aspects of this control. Create something that looks something like what you see below. This is made with three lines, three boxes with radial gradients with the left side of the gradient set to bright green and the right side to transparent. A pen path is used to make the triangle. The active parts we will be working with are the green gradient boxes, which we’ll call Nav1Highlight to Nav3Highlight, the triangle which we’ll call the NavIndicator, and the labels which we’ll call Nav1Label through Nav3Label. The lines are there for artistic purposes and are not active parts of the navigation bar.







  1. Next we need to define the regions that the user will click or mouse over when they interact with the Nav1, Nav2, or Nav3 navigation items. To do this, put a large box over the entire Nav1 region, make have opacity of 0, and label it Nav1ClickTarget. We want the click targets to be see through so the customer can see the region they want to click on but it’s important to accomplish this by setting the opacity to 0 instead of setting it to no fill and no stroke because if you set it to no fill and no stroke it doesn’t generate mouse events and therefore is obviously not useful as a click target. Do the same for Nav2 and Nav3.







  1. By default we’re going to have Nav1 selected, so lets visually set it up this way. Select Nav2Highlight and set visibility to collapsed and do the same for Nav3Highlight.







  1. Now it’s time to add each of our states. Click on the + symbol in the state box and add the state group MouseOverStates. Click the + symbol in the MouseOverStates group and add the states Nav1Highlighted, Nav2Highlighted, and Nav3Highlighted.







  1. Now we’re going to define what each of these states look like. Click on the state Nav2Highlighted. Note the red border around the workspace and the words in the upper right hand corner that says “State recording is on”. This is recording the changes from the base that are required to be in the Nav2Highlighted state. Set the visibility of Nav1Highlight to collapsed, set the visibility of Nav2Highlight to visible, and move the NavIndicator under Nav2. The results should be similar to below. Click on Nav3Highlight and treat it similarly.






  1. Now we’re going to start to link the user actions to the various states. Select the Nav1ClickTarget and go to events in the property panel (events is the little lightning bolt symbol). Under the section MouseEnter, type in Nav1MouseEnter and hit the enter key. This will wake up VS2008 and add a Nav1MouseEnter method to your Page.xaml.cs code behind file. Do the same with Nav2ClickTarget and Nav3ClickTarget.






  1. For each of the methods add the code below so that the VisualStateManager transitions to each of the defined states based on the mouse entering the ClickTarget regions.



        private void Nav1MouseEnter(object sender, MouseEventArgs e)
{
VisualStateManager.GoToState(this, "Nav1Highlighted", true);

}

private void Nav2MouseEnter(object sender, MouseEventArgs e)
{
VisualStateManager.GoToState(this, "Nav2Highlighted", true);
}

private void Nav3MouseEnter(object sender, MouseEventArgs e)
{
VisualStateManager.GoToState(this, "Nav3Highlighted", true);
}



  1. Build and view the test page. What you should see when you mouse over the different regions, the navbar snaps to having the NavIndicator pointing under the moused over item and the highlight pops on. That’s cool, and is a really nice way of demonstrating states, but isn’t very satisfying as an animated navigation bar. Let’s experiment and see if we can make it better by adding transitions between states.


  2. Let’s add a transition between Nav1Highlighted and Nav2Highlighted. Click on the + sign beside Nav1Highlighted and add the transition.






  1. Set the transition time to .3 seconds. Save the changes.







  1. Do a clean project, then rebuild, and then view the test page. What you should see is that the NavIndicator smoothly transitions from Nav1 to Nav2 when you highlight Nav2 (whereas all other transitions jump to the state as before). The problem is a smooth transition isn’t very satisfying visually. I want there to be acceleration. So let’s delete the transition we just created in Expression Blend. To do this, click on the – sign right next to the Nav2Highlighted transition.


  2. Now let’s click on the Nav2Highlighted state and adjust the associated storyboards to something more to our liking. If it’s not already visible, make the timeline visible by clicking on the arrow next to the state box in the Objects and Timeline panel. Move the NavIndicator keyframe to .3 seconds. Click on the key frame and set the Easing in the KeySpline graph to x1 =0, x2=1, y1=1, y2=1 as shown below. This will make the horizontal (i.e. x direction) movement start off slow and then reach full speed. Build the project and view it in a test page. What you should see is the NavIndicator transition to Nav2 should have the type of motion as described above. Note that you get the desired animation effect whether you’re started at Nav1 or Nav3. This is be the animation storyboard for the effect is within the state and is not a separate transition between states as it was when we explicitly defined transitions between states.









  1. Repeat the above process for the Nav3Highlighted state so that it too has the NavIndicator animate into place over .3 seconds with an accelerating movement. Interestingly it doesn’t appear that you can duplicate this same method for the Nav1Highlighted state because for some reason I can’t bring up the easing KeySpline graph for the NavIndicator keyframe. I can’t think of any legitimate reason why this is the case, so it may well be a bug in the June 2008 Preview version of Expression Blend 2.5 that is triggered by the fact that the position of the NavIndicator is the same in the Base state as it is in the Nav1Highlighted state. To get around it I just copied the DoubleAnimationUsingKeyFrames XAML that animated the NavIndicator in one of the other two state and pasted it as a storyboard under the Nav1Highlighted VisualState. I then changed the value of the keyframe at .3 seconds to 0 to indicate that at .3 seconds I want the NavIndicator to be at X position 0. Note in Silverlight animations, objects animate relative to a coordinate system whose origin is that of their original position in the base state. Another thing that should be obvious from looking at the XAML below is that states are nothing more than storyboards, even if sometimes they are one frame storyboards.



<vsm:VisualState x:Name="Nav1Highlighted">
<Storyboard>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="NavIndicator" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
<SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0">
<SplineDoubleKeyFrame.KeySpline>
<KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
</SplineDoubleKeyFrame.KeySpline>
</SplineDoubleKeyFrame>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</vsm:VisualState>



  1. Now that we have the mouse over states animating like we want, the last thing we need is to do is add the functionality of navigating to an html page when clicked. Highlight the Nav1ClickTarget then go to the properties panel and select the events icon (the one with the little lightning bolt). In the MouseLeftButtonDown box type Nav1Clicked. This will activate Visual Studio and add a Nav1Clicked method to your code behind file. Do the same with the Nav2ClickTarget and the Nav3ClickTarget.






  1. In a future tutorial I’m going to show you how to make a custom control and easily customize the destinations that you navigate to, but since this tutorial is focused on the Visual State Manager, I’m just going to hard code that clicking on Nav1 takes you to the other tutorials at my website SilverlightWebApps.com, Nav2 takes you to yahoo.com and Nav3 takes you to google.com. To do this add the System.Windows.Browser.HtmlPage.Window.Navigate calls to the NavClicked methods as shown below:



        private void Nav1Clicked(object sender, MouseButtonEventArgs e)
{
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.silverlightwebapps.com/Tutorials.aspx"));
}

private void Nav2Clicked(object sender, MouseButtonEventArgs e)
{
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.yahoo.com"));
}

private void Nav3Clicked(object sender, MouseButtonEventArgs e)
{
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.google.com"));
}




  1. Build and open the test page in a browser. What you should see is a NavIndicator that accelerates into position whenever you mouse over a Nav area, a NavHighlight that blinks on whenever you mouse over a Nav area, and you should see yourself navigate to a different URL when you click on a Nav area. That’s it, you’re done!



Tutorial Part 2: Turning the Navigation Bar Into A Custom Control




  1. Click on the solution for the project and select Add, New project, Sliverlight Class Library. Call the project NavbarCustomControl







  1. Rename Class1.cs Navbar.cs and allow VS2008 to refactor the class. Make the class derive from control. Assign the DefaultStyleKey to be the new Navbar type.



namespace NavbarCustomControl
{
public class Navbar : Control
{
public Navbar()
{
this.DefaultStyleKey = typeof(Navbar);
}

}
}



  1. Add a text file to the project called generic.xaml (apparently it really does have to be called exactly that)


  2. Change the Build Action to Resource and delete the text in the Custom Tool box







  1. Fill in generic.xaml with the default content for this control. Note the line xmlns:vsm. Because we use the VisualStateManager later in our project we need this line as well. As an example just to get something up and running quickly, the XAML below just displays a rectangle for our custom control.



<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:NavbarCustomControl;assembly=NavbarCustomControl"
xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows">
<Style TargetType="custom:Navbar">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="custom:Navbar">
<Grid x:Name="LayoutRoot">
<Rectangle x:Name="BodyElement" Width="200" Height="100" Stroke="Black"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>



  1. Build the solution. That’s it. You’ve just built a custom control that doesn’t do anything except display a 200x100 rectangle. To see the result of this control, let’s build a custom project where we can use the new control. Go to the solution, right click and select add, new project and add a new Silverlight Application. Call it ControlDemo. Allow VS2008 to add a start page, don’t make it the default page, and allow Silverlight debugging.






  1. Delete the auto generated ControlDemo.aspx. Leave the ControlDemo.html


  2. Go to Page.xaml in Control demo and add the tag <custom:Navbar/> to the page. To be able to add this to the page and have the system make sense of it you need to add the xmlns:custom attribute shown below to the UserControl tag. Also you’ll need to right click on the ControlDemo project and add a reference to the NavbareCustomControl project.



<UserControl x:Class="ControlDemo.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:custom="clr-namespace:NavbarCustomControl;assembly=NavbarCustomControl"
Width="400" Height="300">
<Grid x:Name="LayoutRoot" Background="White">
<custom:Navbar/>
</Grid>
</UserControl>



  1. Build the solution. View ControlDemoTestPage.html is a browser and this is what you should see. (Another good way to see the results of your custom control is to open the Page.xaml of the ControlDemo project in ExpressionBlend. It also tends to give you better error messages.)










  1. That wasn’t so bad was it? We just made a do almost nothing custom control. Now let’s make this a navbar. Go to SkinnableNavbar.xap and copy over the entire Grid element (including all the child elements) from Page.xaml and replace the current Grid in generic.xaml. In other words take just about everything from Page.xaml except the UserControl tag. If you were to build the application right now and then try to look at in Expression Blend you’d get the error AG_E_PARSER_BAD_PROPERTY_VALUE. This is because as you see in the excerpt below we have event handlers defined in the XAML. This is perfectly okay in a user control, but is not allowed in generic.xaml.



<Rectangle x:Name="Nav1ClickTarget" ... MouseEnter="Nav1MouseEnter" MouseLeftButtonDown="Nav1Clicked"/>
<Rectangle x:Name="Nav2ClickTarget" ... MouseEnter="Nav2MouseEnter" MouseLeftButtonDown="Nav2Clicked"/>
<Rectangle x:Name="Nav3ClickTarget" ... MouseEnter="Nav3MouseEnter" MouseLeftButtonDown="Nav3Clicked"/>



  1. Delete the MouseEnter and MouseLeftButtonDown attributes from the XAML and rebuild. Now when you look at the test page, below is what you should see:







  1. Of course, nothing happens when you mouse over the elements or click them, but we’re going to change that now. First cut and paste all the NavMouseEnter and NavMouseClicked event handlers from the SkinnableNavbarXap page.xaml.cs code behind. Then we manually wire the MouseEnter and MouseLeftButtonDown events of each of the click targets to our event handlers. One other trick that you need to know to do the manual wiring of the events is that you can’t use the GetTemplateChild method to find elements from within the constructor. Instead you have to do it later in the OnApplyTemplate method. What you end up with is as follows:



namespace NavbarCustomControl
{
public class Navbar : Control
{
public Navbar()
{
this.DefaultStyleKey = typeof(Navbar);
}

public override void OnApplyTemplate()
{
base.OnApplyTemplate();

// Get pointers to each of our click target elements
FrameworkElement Nav1ClickTarget = (FrameworkElement)GetTemplateChild("Nav1ClickTarget");
FrameworkElement Nav2ClickTarget = (FrameworkElement)GetTemplateChild("Nav2ClickTarget");
FrameworkElement Nav3ClickTarget = (FrameworkElement)GetTemplateChild("Nav3ClickTarget");

// Manually add the MouseEnter events
Nav1ClickTarget.MouseEnter += new MouseEventHandler(Nav1MouseEnter);
Nav2ClickTarget.MouseEnter += new MouseEventHandler(Nav2MouseEnter);
Nav3ClickTarget.MouseEnter += new MouseEventHandler(Nav3MouseEnter);

// Manually add the MouseEnter events
Nav1ClickTarget.MouseLeftButtonDown += new MouseButtonEventHandler(Nav1Clicked);
Nav2ClickTarget.MouseLeftButtonDown += new MouseButtonEventHandler(Nav2Clicked);
Nav3ClickTarget.MouseLeftButtonDown += new MouseButtonEventHandler(Nav3Clicked);

}

private void Nav1MouseEnter(object sender, MouseEventArgs e)
{
VisualStateManager.GoToState(this, "Nav1Highlighted", true);

}

private void Nav2MouseEnter(object sender, MouseEventArgs e)
{
VisualStateManager.GoToState(this, "Nav2Highlighted", true);
}

private void Nav3MouseEnter(object sender, MouseEventArgs e)
{
VisualStateManager.GoToState(this, "Nav3Highlighted", true);
}

private void Nav1Clicked(object sender, MouseButtonEventArgs e)
{
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.silverlightwebapps.com/Tutorials.aspx"));
}

private void Nav2Clicked(object sender, MouseButtonEventArgs e)
{
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.yahoo.com"));
}

private void Nav3Clicked(object sender, MouseButtonEventArgs e)
{
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.google.com"));
}
}
}



  1. Clean and rebuild all. Open the demo page. What you should see is that now the navbar completely animates as it did in the early posted tutorial. Congratulations you now have a completely functional custom control! The only problem is that it’s not very skinnable and there are number of hard coded elements. Let’s next get rid of the hard coded URLs and Navigation text.


  2. To get rid of the hard coded URLs we’re going to add the properties Nav1Url, Nav2Url, Nav3Url. We’ll allow the user of the control to specify these URLs by setting them as attributes of the control XAML tag. We’ll also set a default URL of www.silverlightwebapps.com (my website!) if the user doesn’t specify a tag.



To do this, add a property for Nav1Url as shown below, repeat for Nav2Url and Nav3Url:



        public static readonly DependencyProperty Nav1UrlProperty =
DependencyProperty.Register(
"Nav1Url", typeof(string),
typeof(Navbar), null
);
public string Nav1Url
{
get { return (string)GetValue(Nav1UrlProperty); }
set { SetValue(Nav1UrlProperty, value);}
}



Change the click event to use this newly created property. Repeat this for Nav2Clicked and Nav3Clicked:



private void Nav1Clicked(object sender, MouseButtonEventArgs e)
{
System.Windows.Browser.HtmlPage.Window.Navigate(new Uri(Nav1Url));
}


The default values for the properties are added to generic.xaml as shown below:



<Style TargetType="custom:Navbar">
<Setter Property="Nav1Url" Value="http://www.silverlightwebapps.com"/>
<Setter Property="Nav2Url" Value="http://www.silverlightwebapps.com"/>
<Setter Property="Nav3Url" Value="http://www.silverlightwebapps.com"/>



  1. Now let’s show how a user could use these new properties. Go to page.xaml in the ControlDemo project. Find the Navbar tag and modify it as shown below:



<custom:Navbar Nav1Url="http://www.msdn.com"/>



  1. Rebuild all and open the demo page in a browser. You should see that if you click on Nav1 you go to msdn as specified in you Navbar XAML attribute and if you click on any of the other items you go to the default website.


  2. Okay, let’s take care of one other thing. “Skinnable” means that the visual aspect of the control can change, but the underlying code, in this case the code in Navbar.cs does not change. To make it so Navbar.cs remains the same we need to be a little cleaner in how we wire up events, or to put it better, we need to unwire events from old framework elements before we wire them up to new framework elements. Take a look at the property below. By default our click targets are rectangles, but maybe when a new skin is applied it will be wired up to an ellipse. There is no reason the underlying code has to care. The only thing that needs to happen is that when we switch from the default rectangle to the new element that the events are unwired from the old element and then wired up to the new element. The code below does this.



// Nav1ClickTargetProperty
private FrameworkElement m_nav1ClickTarget = null;
private FrameworkElement Nav1ClickTarget
{
get { return m_nav1ClickTarget;}
set
{
FrameworkElement old_click_target = m_nav1ClickTarget;
if (old_click_target != null)
{
old_click_target.MouseEnter -= new MouseEventHandler(Nav1MouseEnter);
old_click_target.MouseLeftButtonDown -= new MouseButtonEventHandler(Nav1Clicked);

}
m_nav1ClickTarget = value;
if (m_nav1ClickTarget != null)
{
m_nav1ClickTarget.MouseEnter += new MouseEventHandler(Nav1MouseEnter);
m_nav1ClickTarget.MouseLeftButtonDown += new MouseButtonEventHandler(Nav1Clicked);
}
}
}



Follow the above model and create similar properties for Nav2ClickTarget and Nav3ClickTarget.



18. Now we need to use these new properties. Go to OnApplyTemplate and change it to what you see below. With the changes below, when the template is changed as it would when applying a new skin, the property now automatically handle the unwiring of the old events and wiring up the new events.



        public override void OnApplyTemplate()
{
base.OnApplyTemplate();

// Get pointers to each of our click target elements
Nav1ClickTarget = (FrameworkElement)GetTemplateChild("Nav1ClickTarget");
Nav2ClickTarget = (FrameworkElement)GetTemplateChild("Nav2ClickTarget");
Nav3ClickTarget = (FrameworkElement)GetTemplateChild("Nav3ClickTarget");

}




  1. The last thing we need to do to complete our skinnable control is to add some attributes that ExpressionBlend can read so that it can help us (in theory – more about that later) skin our control. These attributes have no affect on the control itself. It is solely something that can be discovered by the development tools via reflection to understand what you as a developer consider the critical parts of the control. For this control the critical part is that there be three click targets and that there be states defined for mousing over these targets. Exactly what these states are and exactly what the click targets are can be customized by the user through the process of skinning.



    [TemplateVisualState(Name = "Nav1Highlighted", GroupName = "MouseOverStates"),
TemplateVisualState(Name = "Nav2Highlighted", GroupName = "MouseOverStates"),
TemplateVisualState(Name = "Nav3Highlighted", GroupName = "MouseOverStates"),
TemplatePart(Name = "Nav1ClickTarget", Type = typeof(FrameworkElement)),
TemplatePart(Name = "Nav2ClickTarget", Type = typeof(FrameworkElement)),
TemplatePart(Name = "Nav3ClickTarget", Type = typeof(FrameworkElement))]
public class Navbar : Control
{




  1. Okay, that’s it. You now have a skinnable custom control. Go ahead and rebuild everything and open up ControlDemoTestPage.html.



Tutorial Part 3: Demonstration of Skinning the New Custom Control




  1. Now that we have created a skinnable custom control, let’s skin it and show by example how much we can change the look of skinnable control and still not have to go back and recompile the underlying control.


  2. Open up page.xaml of your ControlDemo project in ExpressionBlend. Paste a second navbar control on the page. (To see them both you may need to adjust the margins or they may be pasted one on top of the other.)







  1. Right click the second navbar control and select “Edit control parts (Template)”. Select “Edit a copy”. Name the new template “SkinnedNavbar”.






What you should see now if you’re using Expression Blend 2.5 June preview (the latest version available at the time I’m writing this tutorial) is an empty box with a little yellow square in the upper left hand corner, which of course doesn’t look right. If you look at the XAML, however, you can see why. The template copied over for the Navbar is completely empty. If you compare this to the behavior when you try to skin a built-in control such as a scroll bar, when you edit a copy you’ll find the template completely filled with the default control template. When I first saw this, this threw me, but apparently according to the answer I got from Microsoft employee Li-Lun Lou at http://silverlight.net/forums/t/22965.aspx it’s a known bug in the June preview version of Expression Blend 2.5 and will hopefully be fixed in the production release. Because of the bug, Expression Blend only copies over the XAML from built in controls. This means we’re going to have to copy over the default XAML from our generic.xaml by hand. (Yes, this is a real pain in the ass.)






<UserControl.Resources>
<ControlTemplate x:Key="SkinnedNavbar" TargetType="custom:Navbar"/>
</UserControl.Resources>



  1. To copy over the code by hand, you’ll need to do two things. One you need to add the following attribute providing a reference to visual state manager to the UserControl tag:



xmlns:vsm="clr-namespace:System.Windows;assembly=System.Windows"

Secondly you’ll need to paste the control template from generic.xaml as a child of the SkinnedNavbar ControlTemplate tag.

  1. Now go back in Expression Blend, right click on the bottom Navbar and what you should see is what I show below – an editable and therefore skinnable control. Whew! Everything about making the skinnable control seems pretty clean except for that hand cutting and pasting.







  1. Now that you’re this far you can edit this any way you want. As a simple demo, try replacing the word Nav1 with something else. For my own purposes, what I want to do is customize this so that it has the logo for my website. (This is not only a tutorial, this is also me upgrading my website.) What I want to do is replace the background with some artwork I had made. So if you want to exactly duplicate the skinning I did, first add this image to the control demo project. (You can get a better version of the image from the source code attached to this article.)







  1. Next delete everything except the NavHighlight items, the NavIndicator and the NavClickTarget items. An easy way to do this is to click the eyeball icon of each of these items so they disappear, leaving only the items that we want to delete visible. Go ahead and delete those items.


  2. Now add in the background image.







  1. Next we need to position the some of the key elements so they line up with this new image. Position the NavHighlights and ClickTargets. Change the NavIndicator to white, add new text for each of the navigation destinations, and draw a new line underneath the whole thing. When I did this, I had to use the eyeball icon to get things out of my way on occasion. I also had to temporarily make some of the NavHighlights visible that normally won’t be visible. Two other keys to getting this right, was that I needed to send my image to the back and bring the click targets to the front.







  1. Next we need to make sure all the states are properly defined. For the Base state collapse Nav2Highlight and Nav3Highlight. Next select the Nav2Highlighted state. Look at the timeline. Recall from part one of this tutorial that we don’t define the position of navIndicator at time 0, so drag the timeline bar to 300ms. Make sure the NavIndicator is lined up in the middle of the second position. Do the same for Nav3Highlighted.






  1. Add the desired web addresses to Nav2Url and Nav3Url attributes of Navbar tag. Build and you’re done. You’ve skinned your control.

Sunday, August 3, 2008

Using Visual State Manager to create an Animated Navbar

The end result of this project is an animated navigation bar similar to the one at the top of the Silverlight.net website or at the top of my own website. You can see a working example of the end result of this tutorial by following this link.

Download source -- TutorialSkinnableNavbar-VSM.zip, 18.1 KB.

Introduction

This article is a step-by-step tutorial on how to use Silverlight 2.0 to make an animated navigation bar. This article is the second in a series. The end result in all the tutorials in this series is a very similar navigation bar, but with each follow-on article in the series, I introduce more advanced concepts in Silverlight that allow you to accomplish this same task in more elegant and maintainable ways. In the first article, I showed you how to create the navigation bar by manually connecting up Storyboards to mouse events to create the desired animations. In this tutorial, I introduce the VisualStateManager and show you how all that manual work in the first tutorial can instead be handled by the power of the VisualStateManager system.

Background

The project in this tutorial uses Silverlight 2 beta 2, Visual Studio 2008, and the June 2008 Preview of Expression Blend 2.5.

Step-By-Step Tutorial

  1. In VS2008, select a new project, an empty web site. Name the website SkinnableNavbar (Note: although I’m naming the project “Skinnable”, this particular tutorial is focused on demonstrating how to use the VisualStateManager, and this Silverlight control will not be “skinnable” by the end of this tutorial; but, this tutorial is a prerequisite for the next tutorial I’m writing, and in that tutorial, we’ll continue from where we leave off here, and indeed make the control skinnable.)
  2. Right click on the solution, click Add Project, and add a Silverlight Application named SkinnableNavbarXap; leave checked the Add Test Page
  3. Click on page.xaml in SkinnableNavbarXap and open in Expression Blend 2.5.
  4. The first step is to create all the visual aspects of this control. Create something that looks like what you see below. This is made of three lines; three boxes with radial gradients, with the left side of the gradient set to bright green and the right side to transparent. A pen path is used to make the triangle. The active parts we will be working with are the green gradient boxes, which we’ll call Nav1Highlight to Nav3Highlight, the triangle which we’ll call the NavIndicator, and the labels which we’ll call Nav1Label through Nav3Label. The lines are there for artistic purposes, and are not active parts of the navigation bar.
  5. Next, we need to define the regions that the user will click or mouse over when they interact with the Nav1, Nav2, or Nav3 navigation items. To do this, put a large box over the entire Nav1 region, make an opacity of 0, and label it Nav1ClickTarget. We want the click targets to be seen through so the customer can see the region they want to click on; but, it’s important to accomplish this by setting the opacity to 0 instead of setting it to no fill and no stroke, because if you set it to no fill and no stroke, it doesn’t generate mouse events, and therefore is obviously not useful as a click target. Do the same for Nav2 and Nav3.
  6. By default, we’re going to have Nav1 selected, so let's visually set it up this way. Select Nav2Highlight and set visibility to collapsed, and do the same for Nav3Highlight.
  7. Now, it’s time to add each of our states. Click on the + symbol in the state box, and add the state group MouseOverStates. Click the + symbol in the MouseOverStates group, and add the states Nav1Highlighted, Nav2Highlighted, and Nav3Highlighted.


  8. Now, we’re going to define what each of these states look like. Click on the state Nav2Highlighted. Note the red border around the workspace and the words in the upper right hand corner that says “State recording is on”. This is recording the changes from the base that are required to be in the Nav2Highlighted state. Set the visibility of Nav1Highlight to collapsed, set the visibility of Nav2Highlight to visible, and move the NavIndicator under Nav2. The results should be similar to below. Click on Nav3Highlight and treat it similarly.
  9. Now, we’re going to start to link the user actions to the various states. Select Nav1ClickTarget and go to events in the Property panel (events is the little lightning bolt symbol). Under the section MouseEnter, type in Nav1MouseEnter, and hit the Enter key. This will wake up VS2008 and add a Nav1MouseEnter method to your Page.xaml.cs code-behind file. Do the same with Nav2ClickTarget and Nav3ClickTarget.

  10. For each of the methods, add the code below so that the VisualStateManager transitions to each of the defined states based on the mouse entering the ClickTarget regions.

  11. private void Nav1MouseEnter(object sender, MouseEventArgs e)
    {
    VisualStateManager.GoToState(this, "Nav1Highlighted", true);

    }

    private void Nav2MouseEnter(object sender, MouseEventArgs e)
    {
    VisualStateManager.GoToState(this, "Nav2Highlighted", true);
    }

    private void Nav3MouseEnter(object sender, MouseEventArgs e)
    {
    VisualStateManager.GoToState(this, "Nav3Highlighted", true);
    }
  12. Build and view the test page. What you should see when you mouse over the different regions, the navbar snaps to having the NavIndicator pointing under the moused-over item and the highlight pops on. That’s cool, and is a really nice way of demonstrating states, but isn’t very satisfying as an animated navigation bar. Let’s experiment and see if we can make it better by adding transitions between states.
  13. Let’s add a transition between Nav1Highlighted and Nav2Highlighted. Click on the + sign beside Nav1Highlighted, and add the transition.

  14. Set the transition time to .3 seconds. Save the changes.


  15. Do a clean project, then rebuild, and then view the test page. What you should see is that the NavIndicator smoothly transitions from Nav1 to Nav2 when you highlight Nav2 (whereas all other transitions jump to the state as before). The problem is a smooth transition isn’t very satisfying visually. I want there to be an acceleration. So, let’s delete the transition we just created in Expression Blend. To do this, click on the – sign right next to the the Nav2Highlighted transition.
  16. Now, let’s click on the Nav2Highlighted state and adjust the associated storyboards to something more to our liking. If it’s not already visible, make the timeline visible by clicking on the arrow next to the state box in the Objects and Timeline panel. Move the NavIndicator keyframe to .3 seconds. Click on the key frame and set the Easing in the KeySpline graph to x1 =0, x2=1, y1=1, y2=1, as shown below. This will make the horizontal (i.e., x direction) movement start off slow and then reach full speed. Build the project and view it in a test page. What you should see is the NavIndicator transition to Nav2 should have the type of motion as described above. Note that you get the desired animation effect whether you’re starting at Nav1 or Nav3. This is so the animation storyboard for the effect is within the state and is not a separate transition between states as it was when we explicitly defined transitions between states.

  17. Repeat the above process for the Nav3Highlighted state so that it too has the NavIndicator animate into place over .3 seconds with an accelerating movement. Interestingly, it doesn’t appear that you can duplicate this same method for the Nav1Highlighted state, because for some reason, I can’t bring up the easing KeySpline graph for the NavIndicator keyframe. I can’t think of any legitimate reason why this is the case, so it may well be a bug in the June 2008 Preview version of Expression Blend 2.5 that is triggered by the fact that the position of the NavIndicator is the same in the Base state as it is in the Nav1Highlighted state. To get around it, I just copied the DoubleAnimationUsingKeyFrames XAML that animated the NavIndicator in one of the other two states and pasted it as a storyboard under the Nav1Highlighted VisualState. I then changed the value of the keyframe at .3 seconds to 0 to indicate that at .3 seconds, I want the NavIndicator to be at X position 0. Note that in Silverlight animations, objects animate relative to a coordinate system whose origin is that of their original position in the base state. Another thing that should be obvious from looking at the XAML below is that states are nothing more than storyboards, even if sometimes they are one frame storyboards.

  18. <vsm:VisualState x:Name="Nav1Highlighted">
    <Storyboard>
    <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="NavIndicator" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
    <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0">
    <SplineDoubleKeyFrame.KeySpline>
    <KeySpline ControlPoint1="0,1" ControlPoint2="1,1"/>
    </SplineDoubleKeyFrame.KeySpline>
    </SplineDoubleKeyFrame>
    </DoubleAnimationUsingKeyFrames>
    </Storyboard>
    </vsm:VisualState>


  19. Now that we have the mouse over states animating like we want, the last thing we need to do is add the functionality of navigating to an HTML page when clicked. Highlight the Nav1ClickTarget, then go to the properties panel and select the events icon (the one with the little lightning bolt). In the MouseLeftButtonDown box, type Nav1Clicked. This will activate Visual Studio and add a Nav1Clicked method to your code-behind file. Do the same with the Nav2ClickTarget and the Nav3ClickTarget.



  20. In a future tutorial, I’m going to show you how to make a custom control and easily customize the destinations that you navigate to, but since this tutorial is focused on the VisualStateManager, I’m just going to hard-code that clicking on Nav1 takes you to the other tutorials at my website SilverlightWebApps.com, Nav2 takes you to yahoo.com, and Nav3 takes you to google.com. To do this, add the System.Windows.Browser.HtmlPage.Window.Navigate calls to the the NavClicked methods, as shown below:

  21. private void Nav1Clicked(object sender, MouseButtonEventArgs e)
    {
    System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.silverlightwebapps.com/Tutorials.aspx"));
    }

    private void Nav2Clicked(object sender, MouseButtonEventArgs e)
    {
    System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.yahoo.com"));
    }

    private void Nav3Clicked(object sender, MouseButtonEventArgs e)
    {
    System.Windows.Browser.HtmlPage.Window.Navigate(new Uri("http://www.google.com"));
    }

  22. Build and open the test page in a browser. What you should see is a NavIndicator that accelerates into position whenever you mouse over a Nav area, a NavHighlight that blinks on whenever you mouse over a Nav area, and you should see yourself navigate to a different URL when you click on a Nav area. That’s it, you’re done!

History

  • August 2008 -- Initial release.