WynApse Home Page
Home    Blog    About    Contact       
Latest Article:


My Tags:
My Sponsors:






Lifetime Member:

Silverlight Buttons and Pushbutton Discussion




Beta 1 Conversion

This conversion caught me off-guard, since the last two went so smoothly.

I came up against one of the 'breaking' changes that I hadn't had to use so far, and that is the use of "addEventListener", and it's only because of the way I setup the Template button that I even found it.

The previous incarnation of this code setup the Template Button handlers in Load= by making a call similar to this:

button.MouseEnter = "handleMouseEnter";

In Beta 1, that is no longer valid. The way to accomplish this is now:

button.addEventListener("mouseEnter", "handleMouseEnter");

Everything else simply worked!

Original Text

Buttons... everybody uses them every day, but how many people actually study how they work? We get so used to 'pressing' inanimate places on our screens that we get don't really pay attention to everything that is going on. Knowing what goes into fooling everyone into thinking they're "pressing" buttons will go a long way to giving your Silverlight the 'snap' it deserves.

Mike Harsh pointed out in a blog post that the button event was firing on mouse down, not mouse up on Bryant Like's Silverlight sample. I'm sure the mouse-up/mouse-down condition was not on Bryant's mind when he wrote his server control and actually got it to place a button on a Silverlight canvas, which is very cool!, but I'm going to discuss it here in terms of various buttons I've found floating around the Silverlight world and the web in general.

Disclaimer

There's a couple questions that should be coming up about now... such as "Do web pages have to follow the same UI guidelines as Desktop Apps?", or "What is the standard for Silverlight applications?". I'd like to think that I am far from thinking that all apps should look alike, or that Windows 3.0 was the do-all/end-all as far as UI is concerned. At the same time, though, there is a level of expectation that our users have, and if we miss the mark there, we may miss the mark with the customer. Silverlight is heading in brave new directions, and who knows what we may see in the future, but for now I think we need to provide our users with an experience that is at least familiar. Note I didn't say "Like" something, just familiar enough to reach the comfort level.

Ok, I've got that out of my system now!

Default Activity

If you look at the standard HTML button here:, you'll see what looks much like the sort of button you'd find on any desktop application from at least Windows 3.0 through 2K. If you push (and hold down) the button, you'll see that the shading changes and the text shifts. This is done to give you the impression of a 'push', obviously. Under normal circumstances, releasing the button would fire the event for that button. If you push the mouse down on a button, then move the mouse off the button with the mouse still down, the button returns to it's default state. That's a cool way for a button designer to test his/her artwork... push down, pull off the button, and you'll see the state change from default, to pressed, to default.

Thinking about what it's going to take to get that action working correctly is bad enough without adding the next part! The default activity of a 'button' on a desktop or Web page is that if you push down, pull off, then with the mouse still held down, roll back onto the button, the mouse will return to the 'pressed' state. Try it!

Furthermore, the mouse event only takes place if the mouse is released while inside the button artwork. If you push down, pull away from the button and release, nothing happens.

Web Picture Buttons, and others

We've all seen applications on the web, and even some desktop applications, that the developer used a graphical image for a button, like this: and used the 'onclick' handler to take care of the button action. The 'feel' of 'buttons' like that is a little off because you get no 'tactile response' from the button press and release. A hyperlink is actually a better user experience than this type of button, because users are familiar with hyperlinks and how they work.

Taking the hyperlink effect back into buttons, web developers came up with something that desktops didn't have, and that is 'rollovers'. As the mouse rolls over a button image, the image changes giving a very nice effect for the user:, yet still no tactile feedback.

One more example, this one in Silverlight:

If you build a Visual Studio application using the Silverlight template, you'll get a default page with this button:


This button has rollovers and a pressed state, and it handles the other roll-in/roll-out situations just fine. The tactile-feel of the 'click' isn't there, but it isn't always, and is it necessary?

My complaint about this button is this: Click down somewhere on the page, roll into the button, and let the mouse go... oops! ... I don't consider that 'standard' action for a button.

To make a confusing situation worse, desktop applications also have a 'disabled' state where not only will the button not take any mouse events, but it appears grayed out.

That gives us at a minimum, 4 states: Normal, Rollover, Selected, and Disabled. Do we have to deal with all of them? It depends on what you're trying to achieve!

Thinking about States

It seems to me that we have less to do in Silverlight than I had to do at <defunct dotCom>NeoPlanet</> or <defunct non-dotCom>TTI</>, because we're not dealing with a message loop in a desktop setting.

Rollovers are handled very nicely with MouseEnter and MouseLeave messages. The only tricky part being what to do with the mouse up/down situation. If you press down on a button then move away from it, no other button should respond to any messages until the mouse is released. That means we need to capture the mouse on mouse down, plus keep track of which button has the capture.

MouseUp on the desktop is realatively easy because we only allow the event to continue if you're mouse up while in the button that has capture. In Silverlight we have to get more creative to figure that out, but there is a solution based on button state.

Confused yet? -- separate out the states

The rollovers are the simple ones. If no button has the mouse captured, allow whatever rollover effect is appropriate to take place, and don't allow it if the mouse is captured... whew... that's one state and two transitions!

Mouse-down on a button will cause the mouse to be captured until mouse up. Mouse down will set the pressed state. Mouse Leave from a button that has captured the mouse will cause the state to change to default. Mouse Enter a button that has captured the mouse will cause the state to change to pressed.

One more... Mouse Up from a button that has captured the mouse AND the state is Pressed will cause the default state to be displayed and the event to fire.

Try it

Here's a basic Silverlight button with all the states but no 'sparkle'. I built it with a rectangle to make it easy to visualize what's going on


This button has a rollover state where the text changes color. The pressed state uses the rollover color and the button moves down and to the right. Try all the combinations of mouse-on, mouse-off, mouse-release and you'll see that it satisfies all the states mentioned earlier. Moreover, because I'm qualifying what to accept at Mouse-release time, I don't get the bogus click message the one from the VS template gives us.

And, this small piece of Silverlight pointed out another problem with this whole situation:

I put a larger canvas on this button xaml and bounded it with a rectangle so that the button states could be shown. The problem comes in when the mouse exits the canvas entirely. Once we're off the canvas, we have no idea what is going on, so our only recourse is to reset our states. So if you stay inside the bounding rectangle (the canvas), the button states work as designed. Once you exit the canvas, states are reset, and any 'captured' state is lost.

This is important to note in case you're placing Silverlight buttons on a page as I have done here and expect multi-state action to work. If the canvas size == button size, once the mouse leaves the button, you have no idea what is going on, and have to destroy any state information previously retained.

But within a canvas

Within a bounding canvas, everything can work very smoothly as shown here:



The Glass Button

As a reward for making it to the bottom of this long post, I've added the "Glass Button" created by Martin Grayson and explained in a 49-page tutorial on Expression Blend. Bryant Likes used Martin's button inside a server control to place the buttons on a Silverlight canvas. Both of those are very cool, but I had a hard time finding the xaml for the button. I finally extracted it from Bryant's code, and have included it in the canvas above.

When I first saw the Glass Button I thought it had some green color to it because it was against the Aero_Grass background. As it turns out, the button works very cool and allows the background to show through no matter what. I've put an image behind the canvas to show the button's ability to truly appear to be 'glass'.

Very quickly... the Glass Button consists of a white-border rectangle, then 1 pixel inside that is a black-bordered rectangle that has an opaque fill. This helps give the button edge-definition against various colors. Next is a light blue radial gradient to give a 'glow' to the button when rolled into.

Next is the Text for the button followed by a lighter opaque gradient that is clipped to the top half of the button. The rest of the Glass Button xaml handles the animation for the glow effect.

I made the Glass Button code a little different in that it can be moved around the canvas with the mouse. Moving it over various areas of the background image will show off it's effectiveness against any background you might choose.

Oh... and there's no 'pressed' state... if I use this button, you'll probably see me turn off the lighter opaque gradient on the pressed state... but hey, if you use it, figure out something cool and let me know :)

Finally

So do you have to deal with buttons like I've described? I don't know... but I do know that I will. I've always believed that no amount of 'pretty' will make a bad application become preferred, but 'ugly' will make a good application become unused.

JavaScript for producing this page:

<script>
var status;
var isMouseDown = false;
var beginX;
var beginY;
var captured = "";
var state = "Default";


function MouseExitCanvas(sender, args){
captured="";
state = "";
}

function ClickMe(){
alert("ClickMe");
}

function ClickMe2(){
alert("ClickMe2");
}

function templateButton_Loaded(sender, args) {
var button = sender.findName("OriginalTemplateButton");
// button.MouseEnter = "handleMouseEnter";
button.addEventListener("mouseEnter", "handleMouseEnter");
button.addEventListener("mouseLeave", "handleMouseLeave");
button.addEventListener("mouseLeftButtonUp", "handleMouseUp");
button.addEventListener("mouseLeftButtonDown", "handleMouseDown");
}

function handleMouseEnter(sender, eventArgs) {
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
gradientStop1.offset = 1;
gradientStop2.offset = .403;
}

function handleMouseLeave(sender, eventArgs) {
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
gradientStop1.offset = 1;
gradientStop2.offset = .218;
}

function handleMouseUp(sender, eventArgs) {
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
gradientStop1.offset = 1;
gradientStop2.offset = .403;

alert("clicked");
}

function handleMouseDown(sender, eventArgs) {
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
gradientStop1.offset = .7;
gradientStop2.offset = .475;
}

function ButtonMouseEnter(sender, args){
switch(sender.Name.toString()){
case "SingleRectangleButton_":
var ctrl = sender.findName("SingleRectangleButton_text");
ctrl["Foreground"]="#FFFF0000";
if (captured == "SingleRectangleButton")
{
var ctrl = sender.findName("SingleRectangleButton");
ctrl["Canvas.Left"]="1";
ctrl["Canvas.Top"]="1";
state = "Pressed";
}
break;

case "RectangleButton_":
var ctrl = sender.findName("RectangleButton_text");
ctrl["Foreground"]="#FFFF0000";
if (captured == "RectangleButton")
{
var ctrl = sender.findName("RectangleButton");
ctrl["Canvas.Left"]="1";
ctrl["Canvas.Top"]="1";
state = "Pressed";
}
break;

case "TemplateButton":
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
if (captured == "TemplateButton")
{
gradientStop1.offset = .7;
gradientStop2.offset = .475;
state = "Pressed";
}
else
{
gradientStop1.offset = 1;
gradientStop2.offset = .403;
}
break;
}
}

function ButtonMouseLeave(sender, args){
switch(sender.Name.toString()){
case "SingleRectangleButton_":
var ctrl = sender.findName("SingleRectangleButton_text");
ctrl["Foreground"]="#FF000000";
if (captured == "SingleRectangleButton")
{
var ctrl = sender.findName("SingleRectangleButton");
ctrl["Canvas.Left"]="0";
ctrl["Canvas.Top"]="0";
state = "Default";
}
else
{
state = "";
}
break;

case "RectangleButton_":
var ctrl = sender.findName("RectangleButton_text");
ctrl["Foreground"]="#FF000000";
if (captured == "RectangleButton")
{
var ctrl = sender.findName("RectangleButton");
ctrl["Canvas.Left"]="0";
ctrl["Canvas.Top"]="0";
state = "Default";
}
else
{
state = "";
}
break;

case "TemplateButton":
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
gradientStop1.offset = 1;
gradientStop2.offset = .218;
if (captured == "TemplateButton")
{
state = "Default";
}
else
{
state = "";
}
break;
}
}

function ButtonDown(sender, args){
switch(sender.Name.toString()){
case "SingleRectangleButton_":
var ctrl = sender.findName("SingleRectangleButton");
ctrl["Canvas.Left"]="1";
ctrl["Canvas.Top"]="1";
sender.captureMouse();
captured = "SingleRectangleButton";
state = "Pressed";
break;

case "RectangleButton_":
var ctrl = sender.findName("RectangleButton");
ctrl["Canvas.Left"]="1";
ctrl["Canvas.Top"]="1";
sender.captureMouse();
captured = "RectangleButton";
state = "Pressed";
break;

case "TemplateButton":
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
gradientStop1.offset = .7;
gradientStop2.offset = .475;
sender.captureMouse();
captured = "TemplateButton";
state = "Pressed";
break;

}
}

function ButtonUp(sender, args){
switch(sender.Name.toString()){
case "SingleRectangleButton_":
var ctrl = sender.findName("SingleRectangleButton");
ctrl["Canvas.Left"]="0";
ctrl["Canvas.Top"]="0";
if ((captured == "SingleRectangleButton") && (state == "Pressed"))
{
sender.releaseMouseCapture();
captured = "";
alert("SingleRectangleButton Clicked");
}
else
{
sender.releaseMouseCapture();
captured = "";
state = "";
}
break;

case "RectangleButton_":
var ctrl = sender.findName("RectangleButton");
ctrl["Canvas.Left"]="0";
ctrl["Canvas.Top"]="0";
if ((captured == "RectangleButton") && (state == "Pressed"))
{
sender.releaseMouseCapture();
captured = "";
alert("RectangleButton Clicked");
}
else
{
sender.releaseMouseCapture();
captured = "";
state = "";
}
break;

case "TemplateButton":
var gradientStop1 = sender.findName("gradientStop1");
var gradientStop2 = sender.findName("gradientStop2");
gradientStop1.offset = 1;
gradientStop2.offset = .218;
if ((captured == "TemplateButton") && (state == "Pressed"))
{
sender.releaseMouseCapture();
captured = "";
alert("TemplateButton Clicked");
}
else
{
sender.releaseMouseCapture();
captured = "";
state = "";
}
break;

default:
captured = "";
state = "";
break;
}
}

function GlassButtonMouseEnter(sender, args){
var sb = sender.findName(sender.Name + "enter");
sb.begin();
}

function GlassButtonMouseLeave(sender, args){
var sb = sender.findName(sender.Name + "leave");
sb.begin();
}

function onGBMouseDown(sender, mouseEventArgs)
{
// Set the beginning position of the mouse.
beginX = mouseEventArgs.getPosition(null).x;
beginY = mouseEventArgs.getPosition(null).y;

isMouseDown = true;

// Ensure this object is the only one receiving mouse events.
sender.captureMouse();
}

// Stop drag and drop operation.
function onGBMouseUp(sender, mouseEventArgs)
{
isMouseDown = false;

// All all objects to receive mouse events.
sender.releaseMouseCapture();
}

// Reposition object during drag and drop operation.
function onGBMouseMove(sender, mouseEventArgs)
{
// Determine whether the mouse button is down.
// If so, move the object.
if (isMouseDown == true)
{
// Retrieve the current position of the mouse.
var currX = mouseEventArgs.getPosition(null).x;
var currY = mouseEventArgs.getPosition(null).y;

// Reset the location of the object.
sender["Canvas.Left"] += currX - beginX;
sender["Canvas.Top"] += currY - beginY;

// Update the beginning position of the mouse.
beginX = currX;
beginY = currY;
}
}

</script>


xaml for the TemplateButton:

<Canvas xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Loaded="templateButton_Loaded"
MouseLeave="MouseExitCanvas">

<Canvas x:Name="OriginalTemplateButton">
<Rectangle Stroke="#FF8E8E8E" StrokeThickness="2" RadiusX="2" RadiusY="2" Height="23" Width="75">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.5,2.109" EndPoint="0.5,-1.109">
<GradientStop x:Name="gradientStop1" Color="#FFFF9E00" Offset="1"/>
<GradientStop x:Name="gradientStop2" Color="#FFEAEAEA" Offset="0.218"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<TextBlock Canvas.Top="3" Canvas.Left="13" FontSize="12" Foreground="#FF5A5A5A" Text="Click Me" />
</Canvas>

</Canvas>


xaml for the RectangleButton:

<Canvas xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">

<Rectangle Width="200" Height="48" Stroke="SteelBlue" StrokeThickness="1" MouseLeave="MouseExitCanvas" />
<Canvas x:Name="SingleRectangleButton_" Canvas.Left="50" Canvas.Top="12"
MouseEnter="ButtonMouseEnter"
MouseLeave="ButtonMouseLeave"
MouseLeftButtonDown="ButtonDown"
MouseLeftButtonUp="ButtonUp"
>
<Canvas x:Name="SingleRectangleButton" Canvas.Left="0" Canvas.Top="0" Width="100" Height="24" >
<Rectangle Canvas.Top="0" Canvas.Left="0" Width="100" Height="24" Fill="#FFFFFFFF" Stroke="#FF000000" StrokeThickness="1" />

<TextBlock x:Name="SingleRectangleButton_text" Foreground="#FF000000" Text="Rectangle" Canvas.Left="15" Canvas.Top="2"/>
</Canvas>
</Canvas>

</Canvas>


xaml for the large canvas at the bottom (including the Glass Button):

<Canvas
xmlns="http://schemas.microsoft.com/client/2007"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">


<Rectangle Canvas.Left="0" Canvas.Top="0" Width="600" Height="480" Stroke="White" StrokeThickness="1" MouseLeave="MouseExitCanvas" />

<Canvas x:Name="RectangleButton_" Canvas.Top="50" Canvas.Left="50" Width="101" Height="25"
MouseEnter="ButtonMouseEnter"
MouseLeave="ButtonMouseLeave"
MouseLeftButtonDown="ButtonDown"
MouseLeftButtonUp="ButtonUp"
>
<Canvas x:Name="RectangleButton" Canvas.Top="0" Canvas.Left="0" Width="100" Height="24" >
<Rectangle Canvas.Top="0" Canvas.Left="0" Width="100" Height="24" Fill="#FFFFFFFF" Stroke="#FF000000" StrokeThickness="1" />

<TextBlock x:Name="RectangleButton_text" Foreground="#FF000000" Text="Rectangle" Canvas.Left="15" Canvas.Top="2"/>
</Canvas>
</Canvas>

<Canvas x:Name="GlassButton" Canvas.Top="150" Canvas.Left="200" Height="34" Width="180"
MouseLeftButtonDown="onGBMouseDown"
MouseLeftButtonUp="onGBMouseUp"
MouseMove="onGBMouseMove">

<!-- Outer border of the button... just the stroke of the rectangle -->
<Rectangle Width="180" Height="34" RadiusX="17" RadiusY="17" StrokeThickness="1" Stroke="#FFFFFFFF"/>

<!-- Next in black border, and a somewhat opaque fill -->
<Rectangle Canvas.Top="1" Canvas.Left="1" Width="178" Height="32" RadiusX="16" RadiusY="16" Fill="#7F000000" Stroke="#FF000000" StrokeThickness="1" />

<!-- Radial gradient to give a glow to the button when rolled over -->
<Rectangle x:Name="GlassButton_glow" Canvas.Top="2" Canvas.Left="2" Width="176" Height="30" RadiusX="15" RadiusY="15" Opacity="1" >
<Rectangle.Fill>
<RadialGradientBrush>
<RadialGradientBrush.RelativeTransform>
<TransformGroup>
<ScaleTransform ScaleX="1.702" ScaleY="2.243"/>
<TranslateTransform X="-0.368" Y="-0.152"/>
</TransformGroup>
</RadialGradientBrush.RelativeTransform>
<GradientStop Color="#B28DBDFF" Offset="0" />
<GradientStop Color="#008DBDFF" Offset="1" />
</RadialGradientBrush>
</Rectangle.Fill>
</Rectangle>

<TextBlock x:Name="GlassButton_text" Canvas.Left="55" Canvas.Top="5" Foreground="#FFFFFFFF" Text="Move Me" />

<!-- over the top of part of the text is a lighter opaque gradient, clipped to the top half of the button -->
<Rectangle Canvas.Left="2" Canvas.Top="2" Width="176" Height="30" RadiusX="15" RadiusY="15">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.494,0.028" EndPoint="0.494, 0.889" >
<GradientStop Color="#99FFFFFF" Offset="0" />
<GradientStop Color="#33FFFFFF" Offset="1" />
</LinearGradientBrush>
</Rectangle.Fill>

<Rectangle.Clip>
<RectangleGeometry Rect="0,0,176,15"/>
</Rectangle.Clip>

</Rectangle>


<!-- Rectangle for animating the glow effect -->
<Rectangle x:Name="GlassButton_glow_" Width="180" Height="30" RadiusX="15" RadiusY="15" Cursor="Hand" Fill="01FFFFFF"
MouseEnter="GlassButtonMouseEnter" MouseLeave="GlassButtonMouseLeave">


<Rectangle.Triggers>
<EventTrigger RoutedEvent="Rectangle.Loaded">
<EventTrigger.Actions>

<BeginStoryboard>
<Storyboard x:Name="GlassButton_glow_enter" BeginTime="1">
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="GlassButton_glow" Storyboard.TargetProperty="Opacity">
<SplineDoubleKeyFrame KeyTime="00:00:00.30" Value="1" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
<BeginStoryboard>
<Storyboard x:Name="GlassButton_glow_leave" BeginTime="0" >
<DoubleAnimationUsingKeyFrames Storyboard.TargetName="GlassButton_glow" Storyboard.TargetProperty="Opacity">
<SplineDoubleKeyFrame KeyTime="00:00:00.30" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Rectangle.Triggers>
</Rectangle>


</Canvas>


<Canvas x:Name="TemplateButton" Canvas.Top="100" Canvas.Left="150"
MouseEnter="ButtonMouseEnter"
MouseLeave="ButtonMouseLeave"
MouseLeftButtonDown="ButtonDown"
MouseLeftButtonUp="ButtonUp"
>
<Rectangle Stroke="#FF8E8E8E" StrokeThickness="2" RadiusX="2" RadiusY="2" Height="23" Width="75">
<Rectangle.Fill>
<LinearGradientBrush StartPoint="0.5,2.109" EndPoint="0.5,-1.109">
<GradientStop x:Name="gradientStop1" Color="#FFFF9E00" Offset="1"/>
<GradientStop x:Name="gradientStop2" Color="#FFEAEAEA" Offset="0.218"/>
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
<TextBlock Canvas.Top="3" Canvas.Left="13" FontSize="12" Foreground="#FF5A5A5A" Text="Click Me" />
</Canvas>


</Canvas>
Copyright © 2006-2017, WynApse