2008-04-25

WPF custom button templates


WPF Button

I’ve had a bit of a play with WPF for the first time today. I decided to create my own button template. The idea of a template is that you can redefine the visual elements that make up the control.

Create a new WPF application

Now add a button within the grid like so

 <Grid>
  <Button Content="Click me" Width="150" Height="50"/>
 </Grid>

So that we can see what we are designing add a gradient background to the window. Within the <Window> node add

 <Window.Background>
  <LinearGradientBrush>
   <GradientStop Color="Black" Offset="0"/>
   <GradientStop Color="White" Offset="1"/>
  </LinearGradientBrush>
 </Window.Background>


Now to start designing the button template. Within the <Window> node add
 <Window.Resources>
 </Window.Resources>

this is where we will add the template, within that new node add the following

 <ControlTemplate x:Key="PetesButton" TargetType="{x:Type Button}">
 </ControlTemplate>

and then change the button to use this template

 <Button Content="Click me" Width="150" Height="50" Template="{DynamicResource PetesButton}"/>


This creates a control template that targets the "Button" control. As a result the button will disappear, this is because our button template is currently empty. To add a rounded rectangle as the outline for the button by adding a <Border> element.

 <Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" BorderBrush="Black" Background="Black">
 </Border>

Next a <Grid> will be added within that new <Border> element, this is because by default a grid will stack controls on top of each other in a Z order rather than laying them out vertically or horizontally. Within the <Grid> add another border which will act as the client area of the control

 <Grid>
  <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Background="White">
  </Border>
 </Grid>

Next add a nice gradient to this border like so
 
 <Border.BorderBrush>
  <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
   <GradientStop Color="White" Offset="0"/>
   <GradientStop Color="#222222" Offset="1"/>
  </LinearGradientBrush>
 </Border.BorderBrush>

This sets the BorderBrush property for the Border to a LinearGradient which starts at 0.5,0 (X=50%, Y=0%) and ends at 0.5,1 (X=50%, Y=100%). The gradient is made up of two parts, White at offset 0 (the very beginning of the gradient) and #222222 at offset 1 (the very end of the gradient).

As this gradient looks so nice lets remove the Background="White" from the <Border> and inside it add a LinearGradient to the background

 <Grid>
  <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
   <Border.Background>
    <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
     <GradientStop Color="#aaaaff" Offset="0"/>
     <GradientStop Color="#444466" Offset="0.6"/>
     <GradientStop Color="#444444" Offset="1"/>
    </LinearGradientBrush>
   </Border.Background>
  </Border>
 </Grid>


This does exactly the same as the BorderBrush, except there is an addition colour at offset 0.6 (60% of the way along the gradient).

Now we have a pretty looking button, but where is the text? To show the text we need a <ContentPresenter>. Beneath the "BorderUp" element (and within <Grid>) add the following

 <ContentPresenter x:Name="Contents" HorizontalAlignment="Center" VerticalAlignment="Center" Width="Auto" Margin="3,3,3,3"/>

Now you will see the text appear! Run the app, the button looks nice! Click the button, now it looks rubbish! :-)

Below the "BorderUp" element and above the "Contents" element add another border that is the reverse of BorderUp, by this I mean that the colours are switched in the gradient, name it "BorderDown"

 <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
  <Border.BorderBrush>
   <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
    <GradientStop Color="White" Offset="1"/>
    <GradientStop Color="#222222" Offset="0"/>
   </LinearGradientBrush>
  </Border.BorderBrush>
  <Border.Background>
   <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
    <GradientStop Color="#aaaaff" Offset="0"/>
    <GradientStop Color="#444466" Offset="0.6"/>
    <GradientStop Color="#444444" Offset="1"/>
   </LinearGradientBrush>
  </Border.Background>
 </Border>

The button now looks like it is sunk into the form. Change the BorderDown element so that it has no opacity

 <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Opacity="0">

Now to make this sunken border visible when the mouse is pressed down. Just before the closing </ControlTemplate> node add


Within that node add a trigger that reacts to the IsPressed property changing

 <ControlTemplate.Triggers>
  <Trigger Property="IsPressed" Value="True">
   <Setter TargetName="BorderDown" Property="Opacity" Value="1"/>
   <Setter TargetName="Contents" Property="Margin" Value="4,4,2,2"/>
  </Trigger>
 </ControlTemplate.Triggers>


Now try running the app and clicking the button. Much nicer, but I think we can improve on it some more!


Above the </ControlTemplate.Triggers> closing node add

 <ControlTemplate.Resources>
 </ControlTemplate.Resources>

Within this section add a <StoryBoard> node with the name "MouseDownTimeLine"

 <Storyboard x:Name="ButtonDownTimeLine">
 </StoryBoard>

Now we are going to animate a property of type Double, to do this we need a DoubleAnimationUsingKeyFrames node

 <Storyboard x:Key="ButtonDownTimeLine">
  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
   <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
  </DoubleAnimationUsingKeyFrames>
 </Storyboard>

and also the opposite, an animation setting the opacity of BorderDown back to zero

 <Storyboard x:Key="ButtonUpTimeLine">
  <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
   <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
  </DoubleAnimationUsingKeyFrames>
 </Storyboard>

Run the app and click the button. Notice how the same old behaviour is there, this is because we haven’t hooked up the StoryBoard animations to anything yet! Change the <ControlTemplate.Triggers> so that it reads as follows

 <ControlTemplate.Triggers>
  <Trigger Property="IsPressed" Value="True">
   <Trigger.EnterActions>
    <BeginStoryboard Storyboard="{StaticResource ButtonDownTimeLine}"/>
   </Trigger.EnterActions>
   <Trigger.ExitActions>
    <BeginStoryboard Storyboard="{StaticResource ButtonUpTimeLine}"/>
   </Trigger.ExitActions>
  </Trigger>
 </ControlTemplate.Triggers>

This will now start the relevant animation whenever the trigger occurs. However, we have lost some behaviour. Previously the contents would offset when the button was down. This was achieved by changing the margin of the "Contents" from 3,3,3,3 to 4,4,2,2 - to reintroduce this behaviour we need to add another animation within the two StoryBoards. As this property is of type Thickness we will need a <ThicknessAnimationUsingKeyFrames> node.

 <ControlTemplate.Resources>
  <Storyboard x:Key="ButtonDownTimeLine">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
    <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
   </DoubleAnimationUsingKeyFrames>
   <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
    <SplineThicknessKeyFrame KeyTime="00:00:00.025" Value="4,4,2,2"/>
   </ThicknessAnimationUsingKeyFrames>
  </Storyboard>
  <Storyboard x:Key="ButtonUpTimeLine">
   <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
    <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
   </DoubleAnimationUsingKeyFrames>
   <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
    <SplineThicknessKeyFrame KeyTime="00:00:00.25" Value="3,3,3,3"/>
   </ThicknessAnimationUsingKeyFrames>
  </Storyboard>
 </ControlTemplate.Resources>

Now the margin will also animate.


Here is the XAML in its entirety, just so I can copy/paste it myself at a later date :-)

    <Style TargetType="{x:Type Button}">
      <Setter Property="Foreground" Value="White"/>
    </Style>
    <ControlTemplate x:Key="GlassButton" TargetType="{x:Type Button}">
      <Border BorderThickness="1,1,1,1" CornerRadius="4,4,4,4" BorderBrush="Black" Background="Black">
        <Grid>
          <Border x:Name="BorderUp" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4">
            <Border.BorderBrush>
              <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
                <GradientStop Color="White" Offset="0"/>
                <GradientStop Color="#222222" Offset="1"/>
              </LinearGradientBrush>
            </Border.BorderBrush>
            <Border.Background>
              <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
                <GradientStop Color="#aaaaff" Offset="0"/>
                <GradientStop Color="#444466" Offset="0.6"/>
                <GradientStop Color="#444444" Offset="1"/>
              </LinearGradientBrush>
            </Border.Background>
          </Border>
          <Border x:Name="BorderDown" BorderThickness="2,2,2,2" CornerRadius="4,4,4,4" Opacity="0">
            <Border.BorderBrush>
              <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1" >
                <GradientStop Color="White" Offset="1"/>
                <GradientStop Color="#222222" Offset="0"/>
              </LinearGradientBrush>
            </Border.BorderBrush>
            <Border.Background>
              <LinearGradientBrush StartPoint=".5,0" EndPoint=".5,1">
                <GradientStop Color="#aaaaff" Offset="0"/>
                <GradientStop Color="#444466" Offset="0.6"/>
                <GradientStop Color="#444444" Offset="1"/>
              </LinearGradientBrush>
            </Border.Background>
          </Border>
          <ContentPresenter x:Name="Contents" HorizontalAlignment="Center" VerticalAlignment="Center" Width="Auto" Margin="3,3,3,3"/>
        </Grid>
      </Border>
      <ControlTemplate.Resources>
        <Storyboard x:Key="MouseDownTimeLine">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
            <SplineDoubleKeyFrame KeyTime="00:00:00.05" Value="1"/>
          </DoubleAnimationUsingKeyFrames>
          <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
            <SplineThicknessKeyFrame KeyTime="00:00:00.025" Value="4,4,2,2"/>
          </ThicknessAnimationUsingKeyFrames>
        </Storyboard>
        <Storyboard x:Key="MouseUpTimeLine">
          <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="BorderDown" Storyboard.TargetProperty="Opacity">
            <SplineDoubleKeyFrame KeyTime="00:00:00.25" Value="0"/>
          </DoubleAnimationUsingKeyFrames>
          <ThicknessAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Contents" Storyboard.TargetProperty="Margin">
            <SplineThicknessKeyFrame KeyTime="00:00:00.25" Value="3,3,3,3"/>
          </ThicknessAnimationUsingKeyFrames>
        </Storyboard>
      </ControlTemplate.Resources>
      <ControlTemplate.Triggers>
        <Trigger Property="IsPressed" Value="True">
          <Trigger.EnterActions>
            <BeginStoryboard Storyboard="{StaticResource MouseDownTimeLine}"/>
          </Trigger.EnterActions>
          <Trigger.ExitActions>
            <BeginStoryboard Storyboard="{StaticResource MouseUpTimeLine}"/>
          </Trigger.ExitActions>
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>

2008-04-18

"Just send them all home!"

What a great read this link is! It's been some time since I have read anything so funny! Their definition of "British" just makes no sense at all, it would result in people living here for generations having to leave to "go home" and allowing people from Sweden / Denmark to move in.

I couldn't resist, I had to write to them. Not that it will do any good :-)


I think your Red / Grey squirrel example on your website is irrelevant, because as far as I know red and grey squirrels do not interbreed. The complexity of real life is that people of different races do. What should happen so someone with 1 white parent and one black one? What about the son of a man who is two thirds British and a third African, and a woman is half British and half Chinese?

As for indigenous people being the ones who lived here 1,000 years ago your argument here too is illogical. If you say that Celtic, Nordic etc who lived here at the time were automatically British then this would also mean that people from Norway, Sweden, and Denmark are equally British and as a result may come and live in the UK.

Finally I would like to know, where do I stand as an individual? I consider myself English (rather than British), but tracing my family tree reveals that my Great Great Grandfather was an immigrant from Germany, and possibly Jewish. That makes me 1/8th German and not entirely British, is 7 eights acceptible? If so then at what percent does a person become unacceptible? The person in my previous example was 58% British, are they acceptible? What about their children if their mother is "100%" British? Their child would be 79% British, incomparison to myself (87.5% British).

It just doesn't make any sense at all, honest it doesn't. I really think the only people who believe this stuff are just idiots, and yes, I do think immigration into the UK needs "fixing". Your literature and website just smacks of some unintelligble drunk in the pub staggering all over the place saying "Just send 'em all 'ome!"


Pete

2008-04-07

New look

www.capableobjects.com has a new look. Nice!

2008-04-04

Binary response in ASP MVC

Today I wanted to give access to certain files on a website only via my DownloadController. This was so that I could ensure the current user had purchased the item in question first, and also sign any license info into the download aswell.



I tried getting a URL like this to work



http://localhost/download/1/SomeFileName



which would remap to the DownloadController



public void Index(int id, string fileName)





This worked fine, and because the URL ended with "SomeFileName" it would get saved as the correct filename too, but this was no use because SomeFileName has no file extension. As soon as I added .zip on the end the request no longer went via the new HttpHandler in the MVC web extensions. Even when I added it in the <httpHandlers> section of web.config it just wouldn’t work.



My problem was in relying on the url for the filename. This is apprarently not the way it should be done. Instead I should have stuck to the standard URL approach



http://localhost/download/1



and added a special HTTP header known as "content-disposition" to the response, this tells the client what the filename should be. Here is a full example of how to write a binary file to the Response when using the new MVC ASP Web Extensions, and how to have it saved on the client with the correct filename.



public void Index(int id)
{
 IProductRepository productRepository = EcoSpace.GetEcoService<IProductRepository>();
 Product product = productRepository.GetByID(id);
 if (product == null)
 {
  ViewData[GlobalViewDataKeys.ErrorMessage] = "Item not found";
  Response.Redirect("/Account/Home", false);
  return;
 }

 Response.ContentType = "Application/" + Path.GetExtension(product.DownloadUrl).Substring(1);
 Response.AppendHeader("content-disposition", "inline; filename=" + product.DownloadUrl);

 string localFileName = "";
 if (product is Edition)
  localFileName = FilePathUrls.Software;
 else
  if (product is Collateral)
   localFileName = FilePathUrls.Collateral;
  else
   throw new NotImplementedException(product.GetType().Name);

 localFileName = Request.MapPath(localFileName);
 localFileName = Path.Combine(localFileName, product.DownloadUrl);

 FileStream fileStream = new FileStream(localFileName, FileMode.Open, FileAccess.Read, FileShare.Read);
 byte[] data = new byte[fileStream.Length];
 using (fileStream)
  fileStream.Read(data, 0, (int)fileStream.Length);
 Response.BinaryWrite(data);
 Response.End();
}



Thanks go to Phil Haak who pointed me in the right direction and was kind enough to promptly help a complete stranger!

2008-04-03

Silverlight and webservices

First download the binaries you need from here:
http://silverlight.net/GetStarted/

Next run VS2008 and create a new project.

Select the Silverlight node and then the Silverlight Application node.

ProjectName = MySilverlightApp
Tick the checkbox "Create directory for solution"
Click OK

On the wizard page you want the default values:
* Add a new page to the solution for hosting the control
Project type = Web Site
Name = MyWebService

Now delete the two ASPX files, we wont be needing those.

Rename the HTML page to Index.html and set it as the project start page.

Right click the website project and select "Add new item".

Select "Web Service".
Name = DateTimeService.asmx
Click ADD

Change the HelloWorld method to

public DateTime GetServerDateTime()
{
  return DateTime.Now;
}


Right-click the References node on the Silverlight project and select "Add service reference".
Click the "Discover" button, and in the tree view that appears select the DateTimeServer node.
Set the NameSpace to DateTimeServer
Click OK

Now open Page.xaml and enter the following within the <Grid>

<StackPanel>
  <Button x:Name="ButtonGetServerDateTime" Content="Get date time" Click="ButtonGetServerDateTime_Click"/>
  <TextBlock x:Name="TextBlockDateTime" Text="Ready..."/>
</StackPanel>


Note: When you type Click=" you will get the option to hit <TAB> to implement the event handler.

Now open Page.xaml.cs

Add the following private member to the class:
private DateTimeServer.DateTimeServiceSoapClient proxy;


Initialise it in the page’s constructor:
proxy = new MySilverlightApp.DateTimeServer.DateTimeServiceSoapClient();
proxy.GetServerDateTimeCompleted +=
new EventHandler<MySilverlightApp.DateTimeServer.GetServerDateTimeCompletedEventArgs>(proxy_GetServerDateTimeCompleted);


Implement the "Completed" method like so:
TextBlockDateTime.Text = e.Result.ToString();


Finally implement the ButtonServerDateTime_Click method like so:
private void GetServerDateTime_Click(object sender, RoutedEventArgs e)
{
  proxy.GetServerDateTimeAsync();
}


That’s it! Run the app and click the button!