One of the key ideas at the heart of the custom control architecture in formsPlayer is the separation of the data from how it is rendered. XForms lends itself very well to this, since it was defined from the ground up with a Model-View-Controller (MVC) 'approach'. This means that data such as the locations we were dealing with in the simple map control tutorial, can be manipulated in other controls, or even used as the basis for other calculations, without affecting the rendering of the data.
This approach also means that we have a standard way that changes to the data are communicated to and from the controls, saving us the need to keep inventing new event names each time we create a new widget.
Although these points may seem obvious, we often see custom controls that have all of the functionality in the user interface. When this happens we obviously don't have an MVC architecture, since without some data there is nothing to put in a 'view'. That might sound like pedantry but by packing the control full of functionality and dropping the MVC approach, we dramatically reduce the usefulness of our control.
A good example is a clock control. Whether they use SVG, XAML, Java or C#, writers of clock widgets always put the timing element in the control itself. Take the Silverlight clock example; the code involves creating a nice looking analogue clock using XAML, but if we look at the code that actually keeps track of the time, we see this:
<Canvas Opacity="0" x:Name="parentCanvas" Loaded="setClockTime">
<Canvas.Triggers>
<EventTrigger RoutedEvent="Canvas.Loaded"
<EventTrigger.Actions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation x:Name="hourAnimation" Storyboard.TargetName="hourHandTransform" Storyboard.TargetProperty="Angle" From="180" To="540" Duration="12:0:0" RepeatBehavior="Forever"/>
<DoubleAnimation x:Name="minuteAnimation" Storyboard.TargetName="minuteHandTransform" Storyboard.TargetProperty="Angle" From="180" To="540" Duration="1:0:0" RepeatBehavior="Forever"/>
<DoubleAnimation x:Name="secondAnimation" Storyboard.TargetName="secondHandTransform" Storyboard.TargetProperty="Angle" From="180" To="540" Duration="0:1:0" RepeatBehavior="Forever"/>
<DoubleAnimation Storyboard.TargetName="parentCanvas" Storyboard.TargetProperty="Opacity" From="0" To="0.7" Duration="0:0:4"/>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Canvas.Triggers>
...
</Canvas>
This mark-up creates a clock-face and hands for the hours, minutes and seconds (removed from this snippet to make it easier to read), and then animations are created which rotate the hands from 180 degrees to 540 degrees (i.e., a full 360 degree rotation), taking the correct amount of time for each hand--12 hours for the hour hand, 60 minutes for the minute hand, and one minute for the second hand. Although this looks roughly OK as a clock--in a separate document the control is at least initialised with the current time--we have no way of ensuring its accuracy, since once it has been initialised we are dealing only with animations and not the current time. But more important even than the accuracy, since the clock will only ever show the current time, it means that we can't use the same widget in other situations, such as:
However, if we can create a version of this Silverlight clock that does not move the hands itself but instead does nothing more than place the hands at the correct position based on whatever data it is given, then we would be able to re-use the widget in all of the places just mentioned. And in addition, we'd also be able to swap the widget out for a completely different widget in the future, if the need ever arose.
In the next section we'll illustrate these points by showing how we can create a custom clock control, using XAML and Microsoft's Silverlight.
As with our simple map control, our first step is to decide how authors can make use of our control. A typical usage might be:
<xf:output value="'10:10:10'" appearance="xaml:analogue-clock" style="width:150px;height:150px;" />
This will create a static clock, 150px by 150px, showing a time of 12:10:10. We'll also ensure that the control can cope with full time and date formats, such as:
<xf:output value="'20010604T112000Z'" appearance="xaml:analogue-clock" style="width:150px;height:150px;" />
Now we know how authors will use the control, we can go ahead and build it. Once we're finished, it will look something like this:
The definition we'll use for the visual side of the clock is pretty much the same as in the example we saw in the last section. The main change is that we don't need the animations that move the clock hands, since we'll be setting the hands based on the bound data. There is a fourth animation though, which fades in the clock, and we'll keep that.
We won't go into XAML in detail, but one point that needs drawing out is that for each attribute we want to modify, we need to provide a unique name for the element that contains that attribute. In the case of the clock, this has already been done, but were we creating the XAML from scratch we would need to follow this rule. To understand why we need to do this let's look at how the minute hand is defined:
<Path Data="M -4, 16 l 3 70 3 0 2 -70 z" Fill="white">
<Path.RenderTransform>
<TransformGroup>
<RotateTransform x:Name="minuteHandTransform" Angle="180"/>
<TranslateTransform X="150.5" Y="145"/>
</TransformGroup>
</Path.RenderTransform>
</Path>
The actual shape of the hand is defined by the Path element, but its orientation is defined by the Angle attribute on the RotateTransform element. So that we can write code to gain access to the attribute, we need to be able to locate the element, and to do this we use the XAML x:Name attribute, which uniquely names the element. Once we've located the attribute, setting it to some value--based on the time value fed into the control--will automatically update the display of the minute hand.
Our custom control will play the role of interacting with a XAML object on behalf of the backplane. (We'll show how to indicate which XAML file to use, below.) Since much of what is involved in doing this will be common to any control we develop that uses XAML, we have taken the functionality out into a common base class:
<?xml version="1.0"?>
<bindings xmlns="http://www.mozilla.org/xbl">
<binding extends="http://svn.x-port.net/svn/public/samples/chrome/com.microsoft.xaml/xf-pe-value.xbl">
...
</binding>
</bindings>
We saw earlier that positioning the hands of the clock requires setting the Angle attribute on each hand. Whilst we could use code to find this attribute each time we need it, this would make our code very specific, since the method for finding attributes in XAML is very different say, to SVG. Our custom control architecture provides a means to map properties in the XAML object to properties on the custom control. The mapping is handled in the XAML-specific code that we are extending, and all we need to do is specify the relationships:
<binding extends="http://svn.x-port.net/svn/public/samples/chrome/com.microsoft.xaml/xf-pe-value.xbl">
<implementation>
<property name="hourOrientation" element="hourHandTransform" attribute="angle" />
<property name="minuteOrientation" element="minuteHandTransform" attribute="angle" />
<property name="secondOrientation" element="secondHandTransform" attribute="angle" />
With these mappings in place, we can now write code like this:
this.hourOrientation = 30;
which will have the effect of setting hourHandTransform.angle.
The next part of our custom control indicates the XAML file to load:
<property name="secondOrientation" element="secondHandTransform" attribute="angle" />
<constructor>
this.firstChild.source = "analogue-clock.xaml";
</constructor>
Finally, as with our map control, we need to provide an implementation for the setValue method, which will need to crack open the data input, and then use the hours, minutes and seconds to set each of the hands on the clock. First we declare the method and its parameter:
</constructor>
<method name="setValue">
<parameter name="newVal" />
<body>
...
</body>
</method>
</implementation>
</binding>
</bindings>
Next we parse the input value in order to get the hours, minutes and seconds:
<body>
try
{
String(newVal).match( /((\d{4})(\d{2})(\d{2})T)?(\d{2})[\:]?(\d{2})[\:]?(\d{2})[Z]?/ );
var hours = parseInt(RegExp.$5, 10);
var minutes = parseInt(RegExp.$6, 10);
var seconds = parseInt(RegExp.$7, 10);
Now we can work out the angle of each hand. Each hour is 30 degrees, although the display will look better if we also add a sixtieth of that for each minute. Once we have the angle we can use the this.hourOrientation to set the correct attribute in the XAML object:
var seconds = parseInt(RegExp.$7, 10);
var angle = (hours / 12) * 360 + minutes/2;
angle += 180;
this.hourOrientation = angle.toString();
Finally, we can do the same thing for the minutes and seconds:
this.hourOrientation = angle.toString();
angle = (minutes / 60) * 360;
angle += 180;
this.minuteOrientation = angle.toString();
angle = (seconds / 60) * 360;
angle += 180;
this.secondOrientation = angle.toString();
}
catch(e)
{
// some error handling
}
</body>
All we need to do now is to create a bindings file:
<?xml version="1.0" encoding="UTF-16"?> <br:bindings xmlns:br="http://www.x-port.net/bindingresolver/" xmlns:xf="http://www.w3.org/2002/xforms" xmlns:xaml="http://schemas.microsoft.com/winfx/2006/xaml" > <br:binding match="xf:output[@appearance='xaml:analogue-clock']/xf:pe--value" binding="analogue-clock.xbl" /> </br:bindings>
Assuming that we call this file my-bindings.xml then we can now use the binding rules in the normal way:
<head>
<title>Silverlight Clock</title>
<link rel="bindings" href="my-bindings.xml" />
.
.
.
</head>
<body>
<xf:output ref="clock" appearance="xaml:analogue-clock" class="clock" />
</body>
If you have a recent version of formsPlayer installed, as well as a version of Microsoft's Silverlight then you can run this demonstration directly. If you'd like to obtain the source files to edit locally, they are available at http://libxh-apps.googlecode.com/svn/trunk/tutorials/custom-controls/silverlight-clock/.