Contents
In one my last WPF articles, MyFriends (VS2008 competition winner, don't you know?), I used some 3D WPF stuff, and I have to say I quite liked it. Don't get me wrong, I find it hard, but it looks very cool. Anyway, since my last WPF article, I have been on a break to New York, where I met up with Josh Smith (read about it here if you are interested), and have been awarded a CodeProject MVP and a Microsoft C# MVP. Which is truly excellent news, I am well thrilled by both these awards... but I think one is expected to carry on doing good work in order to keep these awards. So I got over my holiday slackness, put the old WPF gloves on again, had a think, and came up with the idea of being able to browse Amazon and represent the results of a search query in a 3D space. Where each 3D item would represent a search item result. These search items would be clickable to allow a deeper view of the related Amazon data to be examined. I also thought this idea had a fair amount of scope.
So, that, in a nut shell, is what this article is all about.
As I say, I will be using the Amazon Web Services (AWS) to gather results that match a query string, and it will be a WPF based solution, and really that is about it. Simple, eh?
However, in order to do this, I wanted to use a 3D type view (because it looks nice), so by the time you read to the end of this article, I would hope that you may know about some (or all) of the following:
- Basics of 3D in WPF
- Basics of 3D animations in WPF
- Using third party Web Services in your WPF apps
- Your configuration tool options when dealing with services
- Using FlowDocuments to create scalable varied layout interfaces
- A little bit about Styles/Templates
The following sub sections will go through what I consider to be the important parts of the demo application.
As this application is fairly 3D based, I thought it may be better to show a small video of what it looks like running. To this end, you may click the video below to see what it all looks like in action.
Click video: Go on, be brave, you know you want to
Some time ago, I saw a link on the coding4fun web site about using Amazon Web Services (AWS) with C#, and I thought it was quite neat. Since then, I have been messing around with WPF and some WCF stuff. I am a big fan of Amazon the shop, so I thought why not try and use the Web Service to create a nice WPF app. So that's exactly what I did, and this article is it.
The first step in using the AWS is to actually register at Amazon's site: Amazon Web Services (AWS). This allows you to obtain a key to be able to use the Web Services. But don't worry, I've already registered and obtained a key, which I will be keeping in the attached demo application so you don't have to register with Amazon. But please don't abuse my key.
Anyway, once you have obtained an AWS key, you are ready to try and use the AWS code in your own applications. With the release of WCF and also VS2008, things have changed slightly when it comes to services and how they can be used. Not much, but enough that I feel I should write a little bit about how to configure the AWS to be used in your own applications.
There are several options/tools available to developers using WCF or Web Services with VS2008. VS2005 is the same as it always was, simply add a web reference. But as I now use VS2008, I will be focusing on that.
By far, the easiest way to get up and running is to simply use VS2008, and use the Service References item within the solution, and "Add Service reference".
From here, you are able to add the URL to any Web Service you would like. The AWS one is http://soap.amazon.com/schemas3/AmazonWebServices.wsdl, so you simply pop that into the wizard address, and bingo, you are in business.
All good so far, isn't it? So we've added a reference to AWS, but how do we use this AWS reference in code? Well, that's pretty easy, actually. Let's see:
using AmazonService;
....
....
private Details[] doAmazonSearch(string keyword, string mode)
{
try
{
KeywordRequest keywordReq = new KeywordRequest();
keywordReq.locale = "us";
keywordReq.type = "lite";
keywordReq.sort = "reviewrank";
keywordReq.mode = mode;
keywordReq.keyword = keyword;
keywordReq.tag = this.SubscriberID;
keywordReq.devtag = this.SubscriberID;
AmazonSearchPortClient ams = new AmazonSearchPortClient();
ProductInfo productInfo = ams.KeywordSearchRequest(keywordReq);
return productInfo.Details;
}
catch { return null; }
}
But wait, isn't there more to this than meets the eye? Well actually, there is a fair bit more to this than meets the eye, there are two vital bits of code / configuration that allow us to simply point and click to the reference and start using AWS. If you didn't know better, you may actually not know or even care about these details. Luckily, I am a chap that both wants to know stuff and does care about the details. So let me tell you some more about these two extra details.
They are in fact:
- Service configuration: Without which the application would not know how to communicate with AWS
- Proxy code: Which is what our application code calls
So what I'm going to do now is talk a little bit about each of these items and how they are created, and what your options are for creating these items.
Service configuration
In order for the application to communicate with AWS, we need to add some entries to the App.Config file. The configuration section that we will need to add is to do with Binding
and Endpoint
. There are several options available in the creation of these sections in the App.Config file. Theses options vary in complexity. So I will start with the easiest.
Option 1: Use VS2008
If you are using VS2008, as soon as you successfully add a Service Reference (as discussed above), you will (if you look for it) find that there will be two new sections added (or maybe even a brand new App.Config file, if you don't already have one) created in the App.Config, to allow the application to communicate with the service. Let's see these, shall we?
="1.0" ="utf-8"
<configuration>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="AmazonSearchBinding"
closeTimeout="00:01:00"
openTimeout="00:01:00"
receiveTimeout="00:10:00"
sendTimeout="00:01:00"
allowCookies="false"
bypassProxyOnLocal="false"
hostNameComparisonMode="StrongWildcard"
maxBufferSize="65536"
maxBufferPoolSize="524288"
maxReceivedMessageSize="65536"
messageEncoding="Text"
textEncoding="utf-8"
transferMode="Buffered"
useDefaultWebProxy="true">
<readerQuotas maxDepth="32"
maxStringContentLength="8192"
maxArrayLength="16384"
maxBytesPerRead="4096"
maxNameTableCharCount="16384" />
<security mode="None">
<transport clientCredentialType="None"
proxyCredentialType="None"
realm="" />
<message clientCredentialType="UserName"
algorithmSuite="Default" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<client>
<endpoint address="http://soap.amazon.com/onca/soap3"
binding="basicHttpBinding"
bindingConfiguration="AmazonSearchBinding"
contract="AmazonService.AmazonSearchPort"
name="AmazonSearchPort" />
</client>
</system.serviceModel>
</configuration>
This is cool, VS2008 created this for us. Nice of it, huh? But we do have other options.
Option 2: Use the SVCConfig Editor
A new thing in VS2008 is the WCF Configuration Editor. OK, it's for WCF, and not Web Services, but in the end, all this tool does is create the relevant configuration sections in a new or existing App.Config file. So, we can bend it to our will and get it to configure our Web Service App.Config sections. This tool is accessible in VS2008 under the Tools -> WCF SVCConfig Editor menu. Let's see some screenshots:
Option 3: Use svcutil.exe
The last option is to use the command line tool svcutil.exe, which is capable of creating an entire App.Config and a proxy class (discussed below) from a single command line. The command line to use is as follows.
To do this with svcutil.exe, we can simply use the following command line:
svcutil.exe http://soap.amazon.com/schemas3/AmazonWebServices.wsdl /language:c#
Proxy code
In order for us to communicate with AWS (or any other Web Service), we need to have some proxy code that knows how to serialize the data and calls. To this end, any call made in C#|VB.NET will always have to call this proxy object. The proxy simply takes our calls, and knows how to call the actual service and get the correct return types etc. If we look at a small section of the generated proxy class, say for the AmazonSearchPortClient
class which I showed earlier on, we can see what we are actually dealing with:
[System.Diagnostics.DebuggerStepThroughAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("System.ServiceModel", "3.0.0.0")]
public partial class AmazonSearchPortClient :
System.ServiceModel.ClientBase<AmazonService.AmazonSearchPor>,
AmazonService.AmazonSearchPort {
public AmazonSearchPortClient() {
}
public AmazonSearchPortClient(string endpointConfigurationName) :
base(endpointConfigurationName) {
}
public AmazonSearchPortClient(string endpointConfigurationName,
string remoteAddress) :
base(endpointConfigurationName, remoteAddress) {
}
public AmazonSearchPortClient(string endpointConfigurationName,
System.ServiceModel.EndpointAddress remoteAddress) :
base(endpointConfigurationName, remoteAddress) {
}
public AmazonSearchPortClient(System.ServiceModel.Channels.Binding binding,
System.ServiceModel.EndpointAddress remoteAddress) :
base(binding, remoteAddress) {
}
public AmazonService.ProductInfo KeywordSearchRequest(
AmazonService.KeywordRequest KeywordSearchRequest1) {
return base.Channel.KeywordSearchRequest(KeywordSearchRequest1);
}
public AmazonService.ProductInfo TextStreamSearchRequest(
AmazonService.TextStreamRequest TextStreamSearchRequest1) {
return base.Channel.TextStreamSearchRequest(TextStreamSearchRequest1);
}
.....
.....
.....
But just how do we get one of these proxy objects?
Well, if you are using VS2008, as soon as you have added the Service Reference (as discussed above), you will (if you look for it) find that you actually have a proxy class in your <your project>Service References\<your service ref name>\ folder. The screenshot below shows the proxy for the attached project. VS2008 creates this automatically:
But what if we don't have VS2008? What can we do? As before, svcutil.exe is a valuable tool in the creation of proxy classes as it was for configuration files.
To do this with svcutil.exe, we can simply use the following command line:
svcutil.exe http://soap.amazon.com/schemas3/AmazonWebServices.wsdl /language:c#
This will result in two items being created: proxy code, and an example App.Config (as discussed earlier). In essence, svcutil.exe is doing the same as VS2008, with the exception that it is not a nice GUI type interface, but rather a command line tool. I will leave the debate of what tool is best up to you. I just wanted to make you aware that you could do the same without having VS2008.
If you are interested in how svcutil.exe can be used with WCF, you may also like to read my other WPF/WCF chat article.
As I am using a third party Web Service (AWS, remember), I am a little cautious. Basically, what I am saying is that, if I make a call to the Amazon Web Service, do I really know how and when it will yield a result to me? I came up with an answer of no to this question. To this end, I had a think about the problem, and formalised it into this requirement: "I would like to be able to call AWS asynchronously, and after some pre-defined time has elapsed, I get a valid result and make use of the AWS result, or I alert the user to the apparent time out".
I feel that pretty much covers what I am trying to do. It is a fairly simple requirement. Let's see how that translates into code, shall we?
internal Details[] FetchAmazonDetailDelegate(string searchword, string category);
...
...
private Details[] FetchAmazonDetail(string searchword, string category)
{
return doAmazonSearch(searchword, category);
}
...
...
try
{
FetchAmazonDetailDelegate fetchDetails = FetchAmazonDetail;
IAsyncResult asynchResult = fetchDetails.BeginInvoke(searchword, category,null, null);
while (!asynchResult.AsyncWaitHandle.WaitOne(5000, false))
{
}
details = fetchDetails.EndInvoke(asynchResult);
if (details != null)
{
}
}
catch
{
MessageBox.Show("Error obtaining amazon results");
}
I think the code is fairly self explanatory. It is basically an asynch call that uses a WaitHandle
to wait for 5 seconds before finishing the asynch call. However, this all needs to be wrapped in a try
-catch
as we may get an Exception
, or it is possible the EndInvoke
may cause something strange to occur.
One of the main things I wanted to do with this app was make it funky. To this end, I think the attached demo app has done a fair job. Basically, when a new Amazon search is entered, a number of 3D Meshes are created which are positioned randomly in 3D space. Then for each one of the meshes, the Point3D
are held. Then for each of the held points, a new Point3DAnimation
is created, which is used to animate the PerspectiveCamera
. It is quite effective. Again, a simple screenshot won't do this justice. You need to check out the What it does / what it looks like section for a better demonstration. This is based (in part) on a moving image animation I saw by Lee Brimelow, who now works for Adobe, so no longer supports his WPF site. Which is a shame as that chap had WPF form.
Anyway, the important code of note is as follows:
private void createViewPortCamera()
{
cam = new PerspectiveCamera();
cam.Position = new Point3D(0,0,10);
cam.FarPlaneDistance = 600;
cam.NearPlaneDistance = 0.1;
cam.FieldOfView = 90;
cam.LookDirection = new Vector3D(0,0,-1);
view3D.Camera = cam;
}
private void createViewPortLight()
{
model = new ModelVisual3D();
model.Content = new AmbientLight();
view3D.Children.Add(model);
}
....
....
p3s = new Point3D[details.Length];
for (int i = 0; i < details.Length; i++)
{
MeshGeometry3D plMesh =
this.TryFindResource("planeMesh") as MeshGeometry3D;
InteractiveVisual3D mv = new InteractiveVisual3D();
mv.IsBackVisible = true;
mv.Geometry = plMesh;
mv.Visual = createAmazonDetail(details[i]);
view3D.Children.Add(mv);
Matrix3D trix = new Matrix3D();
double x = ran.NextDouble() * 50 - 50;
double y = ran.NextDouble() * 2 - 2;
double z = -i * 10;
p3s[i] = new Point3D(x, y, z);
trix.Append(new TranslateTransform3D(x, y, z).Value);
mv.Transform = new MatrixTransform3D(trix);
}
pa = new Point3DAnimation(p3s[0], TimeSpan.FromMilliseconds(300));
pa.AccelerationRatio = 0.3;
pa.DecelerationRatio = 0.3;
pa.Completed += new EventHandler(pa_Completed);
cam.BeginAnimation(PerspectiveCamera.PositionProperty, pa);
fetching = false;
dt.Tick += new EventHandler(dt_Tick);
....
....
private void dt_Tick(object sender, EventArgs e)
{
dt.Stop();
if (count == detailsCount-1) count = 0;
else count++;
pa = new Point3DAnimation(new Point3D(p3s[count].X,
p3s[count].Y + 0.5, p3s[count].Z + 2), TimeSpan.FromMilliseconds(500));
pa.AccelerationRatio = 0.3;
pa.DecelerationRatio = 0.3;
pa.Completed += new EventHandler(pa_Completed);
cam.BeginAnimation(PerspectiveCamera.PositionProperty, pa);
}
private void pa_Completed(object sender, EventArgs e)
{
try
{
pa = new Point3DAnimation(new Point3D(p3s[count].X,
p3s[count].Y + 0.5, p3s[count].Z + 1.6), TimeSpan.FromMilliseconds(3100));
pa.Completed += new EventHandler(dt_Tick);
cam.BeginAnimation(PerspectiveCamera.PositionProperty, pa);
}
catch { }
}
As I stated way back at the start of this article, I had seen in the past an Amazon search app written in C#. But it was WinForms, and this is WPF. So I thought why not go to town. To this end, I am not only using the 3D just described, but I am also allowing the following for the 3D viewport:
- Trackball - Tilt/Zoom
- 2D object interaction of 3D surface
Both of these neat features are available thanks to the awesome 3dTools.Dll which is available right here and was created by the WPF 3D team.
Trackball
The trackball is simply great; you just wrap it around your ViewPort3D
control... basically, the one that hosts the 3D models.
<!---->
<inter3D:TrackballDecorator x:Name="inter3d"
DockPanel.Dock="Bottom" Height="Auto">
<inter3D:Interactive3DDecorator>
<Viewport3D x:Name="view3D"/>
</inter3D:Interactive3DDecorator>
</inter3D:TrackballDecorator>
By using these couple of lines, you are able to tilt and zoom the 3D view port. The left mouse button does the tilt, while the right mouse button does the zoom. I've put a screenshot here, but this really doesn't do it justice. I would look at the video in the What it does / what it looks like section for a better demonstration.
2D object interaction of 3D surface
The ability to place a 2D control on a 3D surface is quite compelling to me. This means that I can animate a camera (which is what I do) through a 3D viewport of 3D meshes, where each of the meshes contains a standard 2D UIElement
such as a StackPanel
that the user can interact with. This again is exactly what I do. I allow the user to click on a 2D UIElement
from a 3D mesh, which in turn launches the Details window (FlowDocumentWindow
), which holds a FlowDocument
discussed next. Let's see some code:
MeshGeometry3D plMesh = this.TryFindResource("planeMesh") as MeshGeometry3D;
InteractiveVisual3D mv = new InteractiveVisual3D();
mv.IsBackVisible = true;
mv.Geometry = plMesh;
mv.Visual = createAmazonDetail(details[i]);
view3D.Children.Add(mv);
....
....
....
private StackPanel createAmazonDetail(Details amazonDetail)
{
StackPanel sp = new StackPanel();
sp.Background = Brushes.Transparent;
AmazonItem item = new AmazonItem(amazonDetail);
item.ItemClicked +=
new AmazonItem.AmazonItemClickedEventHandler(item_ItemClicked);
sp.Children.Add(item);
return sp;
}
Simple, huh? We now have a 2D interactive 2D UIElement
on a 3D mesh. Neat. There was, however, an alternative approach I could have used, which would be to use the new .NET 3.5 Viewport2DVisual3D
class. What this new class allows you to do is to create a 3D model, but it also allows you to host a 2D UIElement
on a 3D Material
, and the 2D UIElement
is fully interactive. So, it's the same as the 3dTools.dll class in functionality, but users would need to have .NET 3.5 installed. I opted for the 3dTools.dll class, as it is something I hadn't used before, and I wanted to play with it. If you want to read more about the .NET 3.5 Viewport2DVisual3D
class, I have created a small demo app, which is available from my blog, right here.
As I have stated throughout this article, each search made to the AWS proxy yields a search result, in the form of an array of Details[]
objects. One thing that may not be obvious is that the demo application allows three different search types to be performed. Books/DVD/video and as such the public properties that are populated with valid data may vary, dependant on what type of search is being performed. For example, one would not expect a DVD to have an author. And, as I wanted to display only those properties that have valid data for the currently viewed Amazon Detail object, I needed a generic way of grabbing only the properties that had data in them. I couldn't iterate the collection of fields in a loop, as I didn't know which fields applied to which search type, and also I couldn't guarantee which properties would actually hold values any way.
So I had a think; of course, I could use some LINQ, couldn't I? LINQ is for querying inline collections. An array of Details[]
objects, just the ticket. But as it's AWS, there are no LINQ Extension Methods available. Anyway, even if there were, LINQ would have still only got me half way there. It had the same inherent problems as using a loop; I would need to know which properties to select, and I only wanted the ones that weren't null or empty.
So I had another think about this... Reflection to the rescue. I could simply use LINQ/Reflection together to query only those properties on the declaring Type
whose properties were not null or empty. Bingo.
Here is the code snippet:
Details det = AmazonDetail;
Type amazonType = det.GetType();
PropertyInfo[] props = amazonType.GetProperties(
BindingFlags.Public | BindingFlags.Instance);
var nonNullProps =
( from prop in props where
prop.GetValue(det, null) != null &&
!prop.GetValue(det, null).ToString().EndsWith("[]")
select new AmazonParameterDetail
{
PropertyName = prop.Name,
PropertyValue = prop.GetValue(det, null).ToString()
}
);
....
....
foreach (AmazonParameterDetail nonNullprop in nonNullProps)
{
....
....
}
System.Windows.Documents.FlowDocument
is a user control that may be placed on a Window
within one of several FlowDocument
reader controls. The FlowDocument
control itself is rather neat, and allows developers to create HTML like designed layout. For example, we can create a FlowDocument
that can have tables/images/paragraphs/hyperlinks etc.
There are several WPF container controls which you may host a FlowDocument
in. These WPF container controls vary in what they provide. Let's see the differences, shall we?
FlowDocumentScrollViewer
: Simply displays the entire document and provides a scroll bar. Like a web page.
FlowDocumentPageViewer
: Shows a document as individual pages, and allows user to adjust zoom level.
FlowDocumentReader
: Combines FlowDocumentScrollViewer
and FlowDocumentPageViewer
into a single control, and exposes text search facilities.
For example, a FlowDocumentPageViewer
is as shown below:
For those who have not come across the FlowDocument
, here is a list of some of the things that can be done with it:
- Allows paragraphing
- Allows anchoring of images
- Allows hyperlinks
- Allows text blocks
- Allows tables
- Allows subscript/superscript text
- Allows
UIElement
s (such as Button
etc.)
- Allows text effects
Think of FlowDocument
(s) as a mini desktop publishing type interface. Though, I'm sure things like Quark are going to yield more flexibility. Nevertheless, the results of FlowDocument
(s) could be thought as being able to create the sort of page publishing type layout. What I'm going to do now is show you how to create a few of the various FlowDocument
elements, both in XAML and in code, as they are little different actually.
Paragraph
In XAML:
<Paragraph FontSize="11">
This page is a simple FlowDocument that is part
of the Windows.Document namespace, and it
has been included in this application, simply
to show how easy it is to create simple Documents
which have Paragrpahs/Links/Images and can be scaled
up/down using the FlowDocumentReader control.
This only really touches the surface of what you
can do with FlowDocument(s) in WPF, you can also
use all sort of text effects, like subscript/superscript/underline.
You can also use tables. In fact
with FlowDocument(s) you can acheive some pretty slick
looking documents. At least this should give you a
a flavour of what can be done. I hope.
</Paragraph>
In C# code-behind:
Paragraph paraHeader = new Paragraph();
paraHeader.FontSize = 12;
paraHeader.Foreground = headerBrsh;
paraHeader.FontWeight = FontWeights.Bold;
paraHeader.Inlines.Add(new Run("Paragraph Text"));
flowDoc.Blocks.Add(paraHeader);
Hyperlinks
In XAML:
<Paragraph FontSize="11">
<Hyperlink Click="hl_Click"
NavigateUri="www.google.com">Click Here</Hyperlink>
</Paragraph>
In C# code-behind:
Paragraph paraValue = new Paragraph();
Hyperlink hl =new Hyperlink(new Run("click here"));
hl.Click += new RoutedEventHandler(hl_Click);
paraValue.Inlines.Add(hl);
flowDoc.Blocks.Add(paraValue);
Embediing UI Elements
In XAML:
<BlockUIContainer>
<Button Width="60" Height="60" Click="Button_Click">
Click me
</Button>
</BlockUIContainer>
In C# code-behind:
BlockUIContainer uiCont = new BlockUIContainer();
Button b = new Button();
b.Width = b.Height = 60;
b.Click += new RoutedEventHandler(Button_Click);
b.Content ="Click me"
uiCont.Child = b;
flowDoc.Blocks.Add(uiCont);
Within the demo application, I have constructed a Window
(FlowDocumentWindow
) that has an embedded FlowDocument
that is shown as the result of clicking one of the 3D Amazon items within the main Window
(Explorer3D
). LINQ/Reflection mentioned above are used to ensure that only the applicable, not empty or null, properties of the current Amazon Details
object are shown.
Of course, this is only touching the surface of what can be done with FlowDocument
s. But it gives you an idea of how flexible the formatting of documents is with WPF.
From the FlowDocument
that is used within the demo application, it is possible to add an item to the favourites ItemsControl
(Templating of which is discussed below). The way this works is that when the favourites (the star) Button
on the FlowDocument
is clicked, a new AmazonParameterDetail
object (which contains the Amazon Details
) is added to the internal ObservableCollection<AmazonFavourite>
favouriteDataItems
field, which is declared within the main Window
(ExplorerWindow
) of the application. As the collection is an ObservableCollection
, the favourites ItemsControl
is updated as soon as the collection changes.
The favourite items are shown in a scrollable ItemsControl
that is shown when the mouse is placed over the gray strip under the main window's title area.
It can be seen that the ScrollViewer
is not a standard ScrollViewer
. This is due to a custom Style
that has been applied to it. You can read about this below, or for more details, you may refer to my blog entry: ScrollViewer Style.
I have only really used Styles / Templates in a couple of areas in this application. The following list outlines these areas:
- Favourites Items Control
DataTemplate
- Favourites Items OrangeGelButton
ControlTemplate
- Favourites Item CloseButton
ControlTemplate
- Favourites Area ScrollViewer Control
Style
I know it's probably a lot of code, but I'll list all the DataTemplate
/ControlTemplate
s and Style
s just so people can see which ones are which. It's sometimes not that easy to see all this in the XAML.
Favourites Area ScrollViewer Control Style
The entire Favourites ItemsControl
is wrapped in a ScrollViewer
control, and a while back, I was looking at the Infragistics WPF showcase, Tangerine, and I was quite jealous about the scrollbars that they managed to use. I mean, styling a Button is one thing, but the ScrollBar is made of lots of nasty different control parts (Part_XXX elements). But anyway, I decided to give it a go. The code contains all the necessary XAML to do the job, of course. Far too much to list here, but if you are really interested in this Style
, you can read more about it at my blog entry: ScrollViewer Style. Anyway, the basic idea is that it alters the way the scrollbar looks.
The result of applying the Style
is as shown below; the new looking ScrollViewer
is on the left:
Favourites Items Control DataTemplate
This looks as follows:
<!---->
<DataTemplate x:Key="favItemsTemplate">
<Button Content="{Binding Price}"
VerticalAlignment="Top" HorizontalAlignment="Left"
Padding="3" Width="130"
Height="30" FontFamily="Arial Rounded MT"
FontSize="12" FontWeight="Normal"
Foreground="#FFEF3800"
Template="{StaticResource OrangeGelButton}"
Margin="5,5,5,5"
Click="btnFavMain_Click">
<Button.ToolTip>
<Border Background="White"
CornerRadius="5,5,5,5" Width="200">
<DockPanel Width="Auto"
Height="Auto" LastChildFill="True">
<Label Margin="2,2,2,2"
VerticalAlignment="Top"
Width="Auto" Height="Auto"
Content="Amazon Favourite"
Background="#FF000000"
FontFamily="Arial Rounded MT"
FontSize="14" Foreground="#FFFFFFFF"
DockPanel.Dock="Top"/>
<TextBlock Margin="2,2,2,2"
Width="Auto" Height="Auto"
TextWrapping="Wrap">
<Run Language="en-gb">You have saved this Amazon
item as a favourite. You can click it to re open it,
or click on the close button to delete it from the
favourites list</Run>
</TextBlock>
</DockPanel>
</Border>
</Button.ToolTip>
</Button>
</DataTemplate>
And the results of this are as shown below (note that this DataTemplate
also uses the OrangeGelButton
and ControlTemplate
).
Favourites Item OrangeGelButton ControlTemplate
This looks as follows:
<!---->
<ControlTemplate x:Key="OrangeGelButton" TargetType="Button">
<Grid Background="#00FFFFFF">
<Border BorderBrush="#FF000000" CornerRadius="6,6,6,6"
BorderThickness="1,1,0,0" Opacity="0.9">
<Border.BitmapEffect>
<BlurBitmapEffect Radius="1" />
</Border.BitmapEffect>
</Border>
<Border BorderBrush="#FFFFFFFF" CornerRadius="6,6,6,6"
BorderThickness="0,0,0.6,0.6" Opacity="0.7" />
<Border Margin="1,1,1,1" CornerRadius="6,6,6,6" Name="background">
<Border.Background>
<LinearGradientBrush EndPoint="0,1" StartPoint="0,0">
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="#FFFBD19E" />
<GradientStop Offset="1" Color="#FFF68F15" />
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Border.Background>
<Grid Margin="1,1,1,1" ClipToBounds="True">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Rectangle Width="{TemplateBinding FrameworkElement.Width}"
Fill="#FFFFFFFF" Opacity="0.34"
Grid.Row="0" />
</Grid>
</Border>
<Border Margin="1,1,1,1"
BorderBrush="#FFFFFFFF" CornerRadius="6,6,6,6"
BorderThickness="0,0,0,0" Opacity="0.3">
<Border.BitmapEffect>
<BlurBitmapEffect Radius="1" />
</Border.BitmapEffect>
</Border>
<Border Margin="1,1,1,1"
BorderBrush="#FF000000" CornerRadius="6,6,6,6"
BorderThickness="0,0,0.6,0.6" Opacity="1">
<Border.BitmapEffect>
<BlurBitmapEffect Radius="1" />
</Border.BitmapEffect>
</Border>
<Image Source="resources/Amazon.png"
Width="60" Height="11" Stretch="Fill"
HorizontalAlignment="Right"
VerticalAlignment="Top" Margin="5,5,50,5"/>
<ContentPresenter Margin="5,13,5,5"
HorizontalAlignment="Left"
VerticalAlignment="Top"
ContentTemplate="{TemplateBinding ContentControl.ContentTemplate}"
Content="{TemplateBinding ContentControl.Content}" />
<Button x:Name="btnSub"
Click="btnDeleteFavourite_Click"
HorizontalAlignment="Right"
Content="X" Margin="0,0,5,0"
Width="20" Height="20"
FontFamily="Arial Rounded MT"
FontSize="11" FontWeight="Normal"
Template="{StaticResource CloseButton}" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="UIElement.IsMouseOver" Value="True">
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<Storyboard.Children>
<ColorAnimation To="#FFFBD19E"
FillBehavior="HoldEnd"
Duration="00:00:00.4000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[0].(GradientStop.Color)" />
<ColorAnimation To="#FFF68F15"
FillBehavior="HoldEnd"
Duration="00:00:00.4000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[1].(GradientStop.Color)" />
</Storyboard.Children>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<Storyboard.Children>
<ColorAnimation To="#FFFAF688"
FillBehavior="HoldEnd"
Duration="00:00:00.2000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[0].(GradientStop.Color)" />
<ColorAnimation To="#FFF6D415"
FillBehavior="HoldEnd"
Duration="00:00:00.2000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[1].(GradientStop.Color)" />
</Storyboard.Children>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="ButtonBase.IsPressed" Value="True">
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<Storyboard.Children>
<ColorAnimation To="#FFFAF688"
FillBehavior="Stop"
Duration="00:00:00.4000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[0].(GradientStop.Color)" />
<ColorAnimation To="#FFF6D415"
FillBehavior="Stop"
Duration="00:00:00.4000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[1].(GradientStop.Color)" />
</Storyboard.Children>
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<Storyboard.Children>
<ColorAnimation To="#FFFAA182"
FillBehavior="HoldEnd"
Duration="00:00:00.2000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[0].(GradientStop.Color)" />
<ColorAnimation To="#FFFD6420"
FillBehavior="HoldEnd"
Duration="00:00:00.2000000"
Storyboard.TargetName="background"
Storyboard.TargetProperty=
"(Panel.Background).(GradientBrush.
GradientStops).[1].(GradientStop.Color)" />
</Storyboard.Children>
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
And the results of this are as shown below (note that this DataTemplate
also uses the CloseButton
and ControlTemplate
).
Favourites Item CloseButton ControlTemplate
This looks as follows:
<!---->
<ControlTemplate x:Key="CloseButton" TargetType="Button">
<Border Opacity="0.5" Name="bord" Margin="0"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
BorderBrush="#FF000000"
BorderThickness="2,2,2,2"
CornerRadius="5,5,5,5"
Padding="0,0,0,0"
Background="#FFFFFFFF">
<ContentPresenter Margin="{TemplateBinding Control.Padding}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
ContentTemplate=
"{TemplateBinding ContentControl.ContentTemplate}"
Content="{TemplateBinding ContentControl.Content}" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="bord"
Property="Background" Value="#FFFC0C0C"/>
<Setter TargetName="bord"
Property="Opacity" Value="1.0"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
And the results of this are as shown below:
Occasionally crashes with Null Reference, but there is no Exception raised in VS2008, and the InnerException
is null
; there is no message at all to indicate what/where or how the error is being raised. As such, I can't track it down. So if anyone finds where the blighter is, please let me know and I will fix the code.
History
- v1.1: 27/02/08: Minor coding change in Styles.
- v1.0: 12/01/08: Initial issue.