Creating Components using WPF
Silverlight is a perspective platform for web solutions. Microsoft has been actively developing it, trying to move Macromedia Flash aside from its current position. In the internet, there is plenty of data containing examples based on Silverlight, which indeed look impressive. However, unfortunately it is really hard to say anything specific on effort involved as well as possible pitfalls. As an object for our experiments, we have selected a control for setting a value in a specific numeric range. What we have finally achieved, you will know a bit later as well as you will learn about the difficulties and pitfalls.
So, let’s formulate the task. The control should provide for:
- Selecting a value from a possible numeric range
- Displaying the selected value
- Displaying the range of possible numeric values
- Catching graphics design.
As for example, the control may look like this:
Main Classes
We should have to be able to define a current value for which the property Value would be responsible. Also, we should know a maximal and minimal value and increment - MinValue
, MaxValue
, Step
. It would be nice to have an option to regulate the positions where the scale begins and ends which will be set by angles - StartAngle
, SweepAngle
. To set up the frame, scale and other items, we will be using a template.
First, we create the basic class Indicator with minimum required set of features: MinValue
, MaxValue
, Step
. We inherit it from the class Control
. Then we create the class CircularIndicator
as a successor of the class Indicator
and define the following properties in it: StartAngle
, SweepAngle
, PointerAngle
. PointerAngle
will be used for binding. Here we will also define the handler for the mouse events; we should react to the mouse clicks, movements to change the position of the pointer.
Here we should point out that in Silverlight 2.0, it is not feasible to define the following methods as OnMouseEnter
, OnMouseMove
, etc. Instead, we have to subscribe for the corresponding events, which is somewhat unusual, however not causing any inconvenience.
The formula to calculate PointerAngle
is relatively simple:
StartAngle + Value * SweepAngle / (MaxValue - MinValue)
That is practically all, so we have to only add events for changing the properties. But, there is one trick here. The thing is, that unlike WPF, the Dependency
property in Silverlight 2.0 allows pointing at only 4 parameters which is obviously not enough. An option to set metadata is missing or, for example, callback function, which would be nice to have in our case, to check properties (CoerceValueCallback
).
Creating a Panel for Scale Representation
Let’s move to the visual part. And the first one which we come across is the way to display the scale. It is clear that a panel can be created, 15 rectangles placed in there as hairlines set at an angle. However, this is not an attractive solution. It would be more easy and elegant to define a class successor of the class Panel
, let’s call it CircularElementPanel
, and through it define the number, elements template, initial and final angles and the circle radius. This would allow creating the elements automatically. Obviously, the class will fit for creating captions for scales as well, which only require adding StartValue
and EndValue
. Nevertheless, it is not that easy since we should also get bound to something in the template to display a value.
We have done it this way:
First we created class DataField
.
public class DataField
{
private object value = null;
public object ElementValue
{
get { return this.value; }
set { this.value = value; }
}
public DataField(object value)
{
this.value = value;
}
}
When creating a separate element, the object DataField
will be assigned to its property DataContext
. A sample code is provided below:
private static void RecreateElements(CircularElementsPanel panel)
{
DataTemplate elementTemplate = panel.ElementsTemplate;
if (elementTemplate == null) return;
double value = panel.StartValue;
double valueStep = (panel.EndValue - panel.StartValue) / (panel.ElementsCount - 1);
panel.Children.Clear();
for (int i = 0; i < panel.ElementsCount; i++)
{
UIElement element = (UIElement)panel.ElementsTemplate.LoadContent();
if (element is FrameworkElement)
{
((FrameworkElement)element).DataContext = new DataField(value + i * valueStep);
}
panel.Children.Add(element);
}
}
Then in the template of the element, a bind can be created as shown below:
<c:CircularElementsPanel.ElementsTemplate>
<DataTemplate>
<TextBlock Width=”50? TextAlignment=”Center” Text=”{Binding ElementValue}”/>
</DataTemplate>
</c:CircularElementsPanel.ElementsTemplate>
Will these elements be positioned along the circle? In order to achieve such an effect, we need to predefine methods ArrangeOverride
and MeasureOverride
on our panel. We have simplified it a bit – use CodeProject PolarPanel
where these methods are predefined and inherited from it. Despite the fact that it has been written for WPF there’re no issues with it, the only thing that needs reworking is property registration. However, we should point out that the panel contains two more Attachable properties – angle
and radius
. The Radius
will be set as the most possible (considering the final panel size), and the angle
will be defined in the method RecreateElements
. Considering the latest changes, the function looks like this:
private static void RecreateElements(CircularElementsPanel panel)
{
DataTemplate elementTemplate = panel.ElementsTemplate;
if (elementTemplate == null) return;
double angle = panel.StartAngle;
double angleStep = panel.SweepAngle / (panel.ElementsCount - 1);
double value = panel.StartValue;
double valueStep = (panel.EndValue - panel.StartValue) / (panel.ElementsCount - 1);
panel.Children.Clear();
for (int i = 0; i < panel.ElementsCount; i++)
{
UIElement element = (UIElement)panel.ElementsTemplate.LoadContent();
SetRadius(element, panel.ElementsRadius);
SetAngle(element, angle + i * angleStep);
if (element is FrameworkElement)
{
((FrameworkElement)element).DataContext = new DataField(value + i * valueStep);
}
panel.Children.Add(element);
}
}
And finally, let’s create another class – CircularGauge
or Gauge
. In this class, there’s only one property – an angle the knob is turned at. When changing the property, we apply RotateTransform
.
Creating Control Template
Below is the full template CircularIndicator
. This should help in resolving issues described further.
<ControlTemplate x:Key=”Indicator” TargetType =”w:CircularIndicator”>
<Grid x:Name=”LayoutRoot” Margin=”0?>
<Rectangle RadiusX=”5? RadiusY=”5? Margin=”0? Stroke=”Black”>
<Rectangle.Fill>
<LinearGradientBrush StartPoint=”0,0? EndPoint=”0,1?>
<LinearGradientBrush.GradientStops>
<GradientStop Color=”Lavender” Offset=”0?/>
<GradientStop Color=”Gray” Offset=”0.1?/>
<GradientStop Color=”LightGray” Offset=”0.5?/>
<GradientStop Color=”Gray” Offset=”0.9?/>
<GradientStop Color=”Lavender” Offset=”1?/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<c:CircularElementsPanel x:Name=”ScaleLabels”
StartValue=”{TemplateBinding MinValue}”
EndValue=”{TemplateBinding MaxValue}” Margin=”50?
StartAngle=”{TemplateBinding StartAngle}”
SweepAngle=”{TemplateBinding SweepAngle}”
ElementsCount=”{TemplateBinding ValuesCount}”
RotateElements=”False”>
<c:CircularElementsPanel.ElementsTemplate>
<DataTemplate>
<TextBlock Width=”50? TextAlignment=”Center”
Text=”{Binding ElementValue}”/>
</DataTemplate>
</c:CircularElementsPanel.ElementsTemplate>
</c:CircularElementsPanel>
<c:CircularElementsPanel StartValue=”{TemplateBinding MinValue}”
EndValue=”{TemplateBinding MaxValue}”
Margin=”80? StartAngle=”{TemplateBinding StartAngle}”
SweepAngle=”{TemplateBinding SweepAngle}”
ElementsCount=”{TemplateBinding ValuesCount}”
RotateElements=”True”>
<c:CircularElementsPanel.ElementsTemplate>
<DataTemplate>
<Rectangle Fill=”Black” Width=”5? Height=”2?/>
</DataTemplate>
</c:CircularElementsPanel.ElementsTemplate>
</c:CircularElementsPanel>
<w:CircularGauge Margin=”90? Template=”{StaticResource Gauge}”
Angle=”{TemplateBinding PointerAngle}”/>
</Grid>
</ControlTemplate>
First we can see is the background layer with gradient fill, further are the scale and its captions and then Gauge. Please notice the number of scale marks we bind to the property ValuesCount
. It can be easily calculated:
((MaxValue - MinValue) / Step) + 1;
Assignment of Binding's Expressions
It would be nice to bind the number of captions and scale marks with ValuesCount
, however, there’s no such possibility. Honestly, we still cannot believe that hours that we have spent on searching for a solution to set expressions to bind type a*b
have not lead us to any result.
The second item that we would like to draw your attention to is Gauge
. What is it for, you may ask. In fact, its template consists of two ellipses. Why not creating RotateTransform
with a value of an angle bound to PointerAngle
property value in the template? However, we somehow cannot do this. Though the expression Angle = “{TemplateBinding PointerAngle}”
does not cause an error at compilation, it does not provide for any positive result too. Why? That’s still a mystery.
Therefore, it is the reason for creating the class CircularGauge
. It is also worth noticing that in WPF, we can easily solve this task using the property RelativeSource
from Binding, however in Silverlight 2.0 it is missing. We would like to hope that it would show up in later versions since without it, anything substantial is hard to be created.
Also, the presence of RelativeSource
would allow for bypassing the issue of setting an expression for binding. The Converter
could have been passed parameterized by some object. The object would define the operation and one of the operands; the second operand would have been defined by the bind field. Unfortunately, there is no such property as Converter
in the TemplateBinding
, therefore the issue cannot be resolved by using it.
Animation
And finally, the third aspect, which is animation. If in WPF we just can create a set of triggers in the control template, let’s say on mouse pointing, and from there starting animation (Storyboard
) however here is another approach. With the help of the attribute TemplatePart
, we define the name and type of the elements which should be included into the control template of the class. Here, we can set the name Storyboard
used at animation and the name of the element where in the resources the Storyboard
is stored.
[TemplatePart(Name = “RootElement”,
Type = typeof(FrameworkElement))] [TemplatePart(Name = “Normal State”,
Type = typeof(Storyboard))]
Further, in the constructor, we subscribe to the events MouseEnter
and MouseLeave
and create the following handlers:
void CircularGauge_MouseLeave(object sender, MouseEventArgs e)
{
FrameworkElement panel = this.GetTemplateChild(“RootElement”) as FrameworkElement;
(panel.Resources[“MouseOver State”] as Storyboard).Stop();
}
void CircularGauge_MouseEnter(object sender, MouseEventArgs e)
{
FrameworkElement panel = this.GetTemplateChild(“RootElement”) as FrameworkElement;
(panel.Resources[“MouseOver State”] as Storyboard).Begin();
}
As a result, when we point at the control with the mouse, animation starts, and when we take the mouse off, animation stops. Animation in our example is implemented on the Gauge
. The Storyboard
then looks this way:
<Storyboard x:Key=’MouseOver State’>
<ColorAnimation Storyboard.TargetName=’Stop’ Storyboard.TargetProperty=’Color’
To=’White’ Duration=”0:0:0.5? AutoReverse=’False’/>
</Storyboard>
Summary
It should also be noticed that despite all of the glitches and difficulties when developing in Silverlight, this could definitely be considered as a huge step forward in interactive, user-friendly and good-looking web interface. Of course, the technology is only growing, however the developer’s instruments are still not yet debugged (to almost all errors the environment responds with one and the same message all the time, often falls into infinity cycles), there are no or almost absent custom options from WPF (triggers in templates, Bindings, animation). However, it has become easier to create management elements for Web, besides Silverlight technology allows for drawing a distinctive line between programming and graphics design.
History
- 25th April, 2008: Initial post