Preface and Thanks
This is the final part to my WPF for Beginners series, and it has been quite a journey even for me. It has taken quite a lot of effort to create this series. And I could not have done it without a few people's help, namely the following
- Robert Ranck: For creating the VB.NET conversions of my C# projects for all of Part 1 - Part 6 articles that make up this series.
- Karl Shifflett: For answering some of our dumb VB.NET queries, and for correcting my spelling and occasional syntax cock-up's. Thanks eagled eyed one... nothing wrong with your eyes... you may not sleep enough, but your eyesight is A1. Don't let anyone tell you different!
- Bea Costa: For allowing me to use her PlanetListbox in Part 6, thanks Bea.
- Paul Stovell: For his excellent WPF ErrorProvider class which is really cool when you are manually binding expression updates (as we are here)
- Rubi Grobler: For the Adding Glass Effect to WPF using Attached Properties article. And the code which I used in this article.
Introduction
I have been working on this app on and off since Part 1, and it has kind of become a labour of love. I wanted to tweak this and that. I am finally happy with it, and I really hope you lot like it as much as I do. I have tried really hard to make it use all the stuff I have covered as part of this article series, which is no mean feat let me tell you. On top of that, I wanted to make it look cool... as I like cool things. So naturally, I went for some Physics driven application. Neato!!!
As I say, this article is the last part (the finale app if you like) where I will be using all the stuff we have learnt along the way. Just to remind you, that means we will be covering all of the following:
This article is actually a sort of joint venture (my first, but hopefully not last one) with my old team leader. Ladies and gentlemen, may I present Mr. Fredrik Bornander. Fredrik is not only the best programmer I have ever met, but he is a very cool guy, whom I really get on with. I also enjoy bouncing ideas of him. We have plans for many, many more articles in this and other areas, so watch out for them.
However, we are where we are, so I think the way I am going to do this article is talk about what the app does, give you a video, and then break it down (basically dissect it) and relate each part of the dissected app to one of the original article series parts. That way, you can see which of this article series you need to read if you want to see how something works in more detail.
As this is a joint article, where Fredrik also created some of the application's code, I will mention what Fredrik did as well. In fact, I am going to get Fredrik to write the words for his part. Of course, as Fredrik is Swedish, his spelling will need to checked (actually for anyone that's read any of my articles, it's probably the other way round.. he should probably check my spelling...though at least I can say "vowels" not "whales" hey Fredrik...ha ha).
One thing that I need to mention is that for this one, there will be no VB.NET version published. It's too much work, and I need to move my attention on to other articles now. Sorry!
Anyway, here is what we are going to cover in this article, but only in C#, sorry again:
Essentially, the demo app is very simple. It uses the standard SQL Server Northindwind database, where a number of Customer objects are first retrieved, and then when requested, a Customer's related Order objects are fetched from the database. Both the Customer and Order objects allow the user to edit their details and have some validation performed to ensure that the entered data is valid. That's pretty much it. But, as we will see, this still has plenty of scope to use all the WPF goodness we have learnt along the way. As I say, we will also mix it up with a pinch of Physics to make it move in weird ways... which we like.
As previously stated, the demo app uses SQL Server (I use SQL Server 2005), but as long as you have the Northwind database installed, it should all be OK whatever version of SQL Server you use. If you don't have the Northwind database, you will need to download and install it from here. Also note that you will need to modify the connection string that the application uses to match your own SQL Server installation. This can be done within the associated app.config file within the "PhysicsHost" project.
="1.0" ="utf-8"
<configuration>
<configSections>
</configSections>
<connectionStrings>
<add name="PhysicsHost.Properties.Settings.NorthwindConnectionString"
connectionString="Data Source=VISTA01\SQLEXPRESS;
Initial Catalog=Northwind;Integrated Security=True"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
I created the entire application and ported Fredrik's Physics code to WPF. It was WinForms based, so Sacha created the necessary changes to it in order to WPF-ify it. But I can't really take much credit for the Physics code. That's Fredrik's baby all the way. Fredrik is really a frustrated games programmer who starts N-many DirectX games a month, but finishes none of them. Ha ha. At least he'll finish this article with me. So yeah... I ported the Physics code to WPF, but also did all the other WPF elements that make up this demo application. This includes Layout/Resources/Commands and Events/DPs/DataBinding/Styles and Templates, and also LINQ to SQL. Oh, and I also created the trivial DashedOutlineCanvas
within the Physics project.
Sacha used to work with Fredrik, and a while back at work, Fredrik started working on this Physics thing (whilst he should have been doing what he was paid to do, which is write boring Sybian C++, but hey), which kind of peaked Sacha's interest. This Physics thing that Fredrik was working on later became this CodeProject article. But Sacha thought this could be used within a WPF app, so Sacha and Fredrik set about making that happen. As a result, the Physics stuff you see in this application is based on the original Physics stuff that Fredrik did for his original CodeProject article. Nice one Fredrik.
Due to the nature of Physics, the only way that I can do the attached demo application any justice, at all within the scope of this article, is to show you a video which shows it in action. As such, please click on the image below to see a video of the demo application in action:
OK, now I have told you what the app does, told you what you need to try it at home, and shown you a video of the demo app in action. I will not talk you through how it was made. Like I say, I think the best way is to dissect the app and relate it back to the individual articles so that if you are lost or maybe are new to this series, you can go back and have a look at the relevant article part.
The application is structured using two projects: the Physics engine and the WPF application. This is shown below:
These two projects will be discussed in detail below. The WPF project has subfolders which contain various files; the folder name gives you an idea of what the files are for.
This section was written by Fredrik Bornander and proof read and inserted/added to by Sacha Barber.
By using simple Physics to layout controls on a panel, it is easy to get an application to have a very different feel than applications using "normal", static layout of its controls.
The aim with this Physics implementation is to create an easy way for developers without any experience in Physics programming to be able to build cool looking applications.
By creating a control, in this case, a subclass of Canvas
(which is called ParticleCanvas
) that can be used as any other control in a window, it's simple to add Physics controlled controls to any UI.
Any control added to ParticleCanvas
can then be related to a Physics Particle
; Spring
s are then added to constrain the particles in whatever configuration is desired. It should be noted that the controls should really only be added to the ParticleCanvas
in the code-behind where they are allocated to a Particle
and attached to a Spring
. If they are added in XAML, there would still need to be some code added in the code-behind to attach the controls to Particle
s.
ParticleCanvas Class
ParticleCanvas
is the canvas control that owns ParticleSystem
(Physics system) It uses a DispatcherTimer
to regularly update the state of its internal ParticleSystem
(Physics system) so that it can be animated. It does this by telling the internal ParticleSystem
(Physics system) to do an integration using the elapsed time; an integration is the operation in which the ParticleSystem
s (Physics system) next state is computed. This is done once for every timer "tick", and after each integration, all the controls that are related to a Physics Particle
are relocated to the Particle
's new position. This is all taken care of in the HandleWorldTimerTick
method:
private void HandleWorldTimerTick(object sender, EventArgs e)
{
Rect constaintsRectancle =
new Rect(0, 0, this.ActualWidth, this.ActualHeight);
lock (ParticleSystem.Particles)
{
ParticleSystem.DoEulerStep(0.005f, constaintsRectancle);
foreach (Particle particle in ParticleSystem.Particles)
{
particle.SnapControl();
}
}
this.InvalidateVisual();
}
The final call to this.InvalidateVisual()
forces ParticleCanvas
to redraw its controls, which snaps the Particle
s to their new positions.
As the user is able to drag Phyics constrained controls around using the mouse, ParticleCanvas
has to keep track of a series of mouse events.
PreviewMouseDown
, PreviewMouseMove
, and PreviewMouseUp
are all used to handle control dragging.
During PreviewMouseDown
, the list of Physics Particle
s are queried for a Particle
with the event sender control related to it; i.e., a search is done to see if there is a Physics Particle
that is currently related to the control that received the mouse event. If this is the case, that control is brought forward along the Z-Index to make it appear on top of other controls, and the related Physics Particle
gets its current velocity cleared and its mass set to positive infinity. The reason for resetting the mass is that Particle
s with an infinite mass is ignored by the ParticleSystem
(Physics system) when the integration is done, and this is important as only the mouse should control the movement of that Particle
now.
public void ParticleCanvas_PreviewMouseDown(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed && ownerWindow != null)
{
previousAbsoluteMousePosition = Mouse.GetPosition(this);
Vector mousePosition = previousAbsoluteMousePosition.ToVector();
var particleWhere = from particle in ParticleSystem.Particles
where particle.Control == sender
select particle;
if (particleWhere.Count() > 0)
{
Particle particle = particleWhere.First();
if (selectedParticle != null)
selectedParticle.Mass = selectedParticleMass;
selectedParticleMass = particle.Mass;
selectedParticle = particle;
selectedParticle.Mass = Single.PositiveInfinity;
selectedParticle.Velocity = new Vector();
selectedParticle.Control.SetValue(Canvas.ZIndexProperty, zIndex++);
return;
}
}
}
During PreviewMouseMove
, the distance the mouse cursor has travelled is measured, and the Particle
is updated with the same movement; the control itself will have its position updated automatically by the next timer tick.
public void ParticleCanvas_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
if (selectedParticle != null)
{
Point absolutePosition = Mouse.GetPosition(this);
Rect constaintsRectancle =
new Rect(0, 0, this.ActualWidth, this.ActualHeight);
selectedParticle.SetPosition(
new Vector(
selectedParticle.Position.X +
(absolutePosition.X - previousAbsoluteMousePosition.X),
selectedParticle.Position.Y +
(absolutePosition.Y - previousAbsoluteMousePosition.Y)),
constaintsRectancle
);
previousAbsoluteMousePosition = absolutePosition;
}
}
}
When the mouse is released and the PreviewMouseUp
event fires, the Particle
's original mass is restored; this will (if the original mass wasn't positive infinity) allow the ParticleSystem
(Physics system) to again move the particle (and therefore also its related control) around.
public void ParticleCanvas_PreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (e.LeftButton == MouseButtonState.Released)
{
if (selectedParticle != null)
{
selectedParticle.Mass = selectedParticleMass;
FireParticleReleasedEvent(selectedParticle);
selectedParticle = null;
}
}
}
The ParticleCanvas
also (optionally) keeps track when the window it is contained in is being moved so that the Particle
s can be updated accordingly; this allows for a natural behaviour of the controls suspended by Spring
s as they'll swing back to their state of rest. The ParticleCanvas
does this by moving all the movable (those with mass not equal to positive infinity) Particle
s by the negative distance the window just moved.
This means that if the window was dragged ten pixels to the left, the Particle
s would be relocated 10 pixels to the right. Try it for yourself; it looks quite cool.
public void HandleOwnerWindowMove(object sender, EventArgs e)
{
Vector deltaMovement = new Vector(ownerWindowPosition.X -
ownerWindow.Left, ownerWindowPosition.Y - ownerWindow.Top);
ownerWindowPosition = new Point(ownerWindow.Left, ownerWindow.Top);
foreach (Particle particle in ParticleSystem.Particles)
{
particle.MovePosition(deltaMovement);
}
}
ParticleSystem Class
The ParticleCanvas
' internal Physics simulation is maintained by a ParticleSystem
instance. The ParticleSystem
is a class which owns all Particle
s and Spring
s, holds all world properties (such as gravity and drag), and computes the integration. The integration is calculated in two major steps:
Step 1: Calculate the Derivatives for All Particles
This means calculating the difference in velocity and position, which is acceleration and velocity. First, all existing forces that are currently acting upon a Particle
are cleared so that the Particle
is completely unaffected by force. Then the "world" forces are applied; this is the force of gravity and the drag which is kind of like wind resistance. The drag is important to simple simulations as this one, as it puts a "damper" on the system which stabilizes it. After that, the forces that the Spring
s add to the Particle
s are applied to the Particle
s, and after that, the just calculated "state" is stored away in the Particle
as a ParticleState
.
public void CalculateDerivative()
{
foreach (Particle particle in Particles)
{
particle.ResetForce();
particle.AddForce(Gravity);
Vector drag = particle.Velocity * -dragFactor;
particle.AddForce(drag);
}
foreach (Spring spring in Springs)
{
spring.Apply();
}
foreach (Particle particle in Particles)
{
particle.State = new ParticleState(particle.Velocity,
particle.Force * (particle.OneOverMass));
}
}
Step 2: Update the Position with the Particle State
First, the Particle
state is scaled down by the time factor so that the update will be proportional to the time elapsed. After that, the Particle
velocity is updated by simply adding the state velocity to the Particle
's current velocity. The same normally applies for position as well, but as the Particle
s can be constrained to the ParticleCanvas
's rectangle, a few calculations have to be done to make sure it bounces off the edges if it is moving off the screen.
public void DoEulerStep(double deltaTime, Rect constaintsRectancle)
{
CalculateDerivative();
foreach (Particle particle in Particles)
{
particle.State.Position *= deltaTime;
particle.State.Velocity *= deltaTime;
particle.Velocity = particle.Velocity + particle.State.Velocity;
Vector newPosition = particle.Position + particle.State.Position;
if (particle.ConstrainedToCanvas &&
!constaintsRectancle.Contains(newPosition.ToPoint()))
{
double x = particle.Velocity.X;
double y = particle.Velocity.Y;
if (particle.Velocity.X < 0 && newPosition.X
< constaintsRectancle.Left)
{
newPosition.X = constaintsRectancle.Left;
x *= -(1.0 - wallFriction);
}
if (particle.Velocity.X > 0 && newPosition.X
> constaintsRectancle.Right)
{
newPosition.X = constaintsRectancle.Right;
x *= -(1.0 - wallFriction);
}
if (particle.Velocity.Y < 0 && newPosition.Y
< constaintsRectancle.Top)
{
newPosition.Y = constaintsRectancle.Top;
y *= -(1.0 - wallFriction);
}
if (particle.Velocity.Y > 0 && newPosition.Y
> constaintsRectancle.Bottom)
{
newPosition.Y = constaintsRectancle.Bottom;
y *= -(1.0 - wallFriction);
}
particle.Velocity = new Vector(x, y);
}
particle.Position = newPosition;
}
}
The ParticleSystem
class also exposes a method for rendering the Spring
s.
public void Render(System.Windows.Media.DrawingContext dc)
{
lock(Springs)
{
foreach (Spring spring in Springs)
{
spring.Render(dc);
}
}
}
Particle Class
The ParticleSystem
class is responsible for calculating positions and velocities of Particle
s. Particle
s are rather simple classes that hold position, velocity, force, and mass used when doing the simulation, and also, optionally, a relation to a Control
. If a Control
is related to a Particle
, that Control
's position is aligned with the Particle
when the method SnapControl
is called.
public void SnapControl()
{
if (Control != null)
{
Control.SetValue(Canvas.LeftProperty,
(double)Position.X - Control.ActualWidth / 2.0);
Control.SetValue(Canvas.TopProperty,
(double)Position.Y - Control.ActualHeight / 2.0);
Control.Arrange(new Rect(Position.ToPoint(), Control.DesiredSize));
}
}
Other than that, the Particle
doesn't hold much logic; it's all calculated by the ParticleSystem
class. If a Particle
is assigned a mass of positive infinity, it is not affected by any force; this is useful when creating anchor points in the simulation. Note that even if the Particle
has positive infinity mass, it can still be moved by dragging it with the mouse.
Spring Class
Spring
s are used to constrain two Particle
s by applying a force to the Particle
s so that the Spring
's properties are satisfied. The properties of the Spring
are:
- Rest length: This is the distance the spring will eventually end up in if the
Particle
s are affected by no other forces.
Spring
constant: This is a measure of how stiff the Spring
is; the higher the value, the more "eager" to reach its Rest length.
- Damping constant: This is a damper that is used to make the
Spring
move slower and stabilize the simulation.
The forces that the Spring
applies to its two Particle
s are calculated by a method that might look a bit complicated, but really is quite simple:
public void Apply()
{
Vector deltaX = From.Position - To.Position;
Vector deltaV = From.Velocity - To.Velocity;
double term1 = SpringConstant * (deltaX.Length - RestLength);
double term2 = DampingConstant *
Vector.AngleBetween(deltaV, deltaX) / deltaX.Length;
double leftMultiplicant = -(term1 + term2);
Vector force = deltaX;
force *= 1.0f / deltaX.Length;
force *= leftMultiplicant;
From.Force += force;
To.Force -= force;
}
Calculate the direction the force should have, this is always in the direction of the other Particle
; apply the Spring
constant to the difference in distance between Particle
s and the desired distance. Figure out the amount of damping needed, this is relative to how much or how little the Particle
s are moving away from each other; the more they're moving away, the more the damping. Calculate the force to apply to the first particle, negate the force, and apply it to the second Particle
.
The Spring
can also render itself using a ISpringRenderer
; this is just a way to get different looks for a Spring
.
You can refer to Part 1 for more information about Layouts in WPF.
Layout is literally used everywhere within the demo app. From the Windows to the UserControls to the Custom Styles/Templates. It's everywhere. Probably the best way is to pick a couple of Windows and provide screenshots and the associated layout markup that creates the Windows. Though, there are far too many areas where Layout is used to go into everything. But this should give you a good idea.
There are four Windows within the demo app:
And there are two User Controls, though I won't discuss the layout for these just yet, as the discussion about the UserControls is more suited to Templates/Styles and Lookless controls.
If we pick on two or three of these Windows, say the following ones, we should be able to discuss enough layout I think:
- MainWindow.xaml
- EditOrderWindow.xaml
- AboutWindow.xaml
OK, so MainWindow.xaml looks like:
Now we will forget about the ParticleCanvas
in the middle (the yellow boxed area) just for a moment, as Fredrik should have talked about this a bit in the Physics section, and I will talk about the layout aspects a bit more below. Once we gloss over the ParticleCanvas
, the layout is failry trivial (I have removed some markup for clarity).
<Window x:Class="PhysicsHost.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:physics="clr-namespace:BarberBornander.UI.Physics;
assembly=BarberBornander.UI.Physics"
xmlns:models="clr-namespace:PhysicsHost.ViewModel"
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
WindowState="Maximized"
WindowStartupLocation="CenterScreen"
Title="Particles" Height="800" Width="600"
Icon="../Images/logo.png"
Loaded="MainWindow_Loaded"
SizeChanged="Window_SizeChanged">
<Window.Resources>
....
....
</Window.Resources>
<Window.ContextMenu>
<ContextMenu>
<MenuItem Tag="../Images/anchor.png"
Header="Reset Anchor To Start Position"
Template="{StaticResource contentMenuItemTemplate}"
Click="MenuItem_Click" />
</ContextMenu>
</Window.ContextMenu>
<Window.CommandBindings>
....
....
</Window.CommandBindings>
-->
<Grid x:Name="LayoutRoot" Background="Black">
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="30"/>
</Grid.RowDefinitions>
-->
<physics:DashedOutlineCanvas Margin="20,0,0,20"
Background="#FFFF9900" Grid.Column="0"
Grid.Row="1"
MouseDown="DashedOutlineCanvas_MouseDown"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Width="400" Height="20">
<Label FontFamily="Arial" FontSize="10"
FontWeight="Bold" Foreground="Black"
Content="FileInfo:// A WPF particle system
by Sacha Barber + Fredrik Bornander"/>
</physics:DashedOutlineCanvas>
<DockPanel Background="Black"
LastChildFill="True" Grid.Column="0"
Grid.Row="0" Margin="0,0,0,10">
-->
<Border DockPanel.Dock="Top"
CornerRadius="10,10,0,0"
Height="120" Margin="10,10,10,0"
Background="{StaticResource orangeGradientBrush2Stops}">
<Image Source="../Images/header.png"
HorizontalAlignment="Left"
VerticalAlignment="Top" Width="480"
Height="90" Margin="15,15"/>
</Border>
-->
<Border DockPanel.Dock="Bottom"
CornerRadius="0,0,0,0" Margin="10,0,10,0">
<Border.Background>
<LinearGradientBrush
EndPoint="0.484,0.338"
StartPoint="0.484,0.01">
<GradientStop Color="#FFFF9900" Offset="0"/>
<GradientStop Color="#FF000000" Offset="1"/>
</LinearGradientBrush>
</Border.Background>
<physics:ParticleCanvas DockPanel.Dock="Bottom"
x:Name="particleCanvasSimulation"
Margin="10,10,10,10"
Width="Auto" Height="Auto">
<TextBlock x:Name="txtRemoveOrders"
FontSize="14" FontStyle="Italic"
FontWeight="Bold" Foreground="White"
Canvas.Left="10" Canvas.Top="10"
TextDecorations="Underline"
Text="Remove All Orders"
Visibility="Hidden"
MouseDown="txtRemoveOrders_MouseDown"/>
</physics:ParticleCanvas>
</Border>
</DockPanel>
</Grid>
</Window>
OK, so EditOrderWindow.xaml looks like:
Here is the layout for this Window (again, I've removed certain markup for clarity):
<Window x:Class="PhysicsHost.EditOrderWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:PhysicsHost.ViewModel"
xmlns:validation="clr-namespace:PaulStovell.Samples.WpfValidation"
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
Icon="../Images/logo.png"
Title="Particles" Height="360" Width="500"
ResizeMode="NoResize"
Background="Black" TextElement.Foreground="White">
<Window.Resources>
....
....
</Window.Resources>
<Window.CommandBindings>
....
....
</Window.CommandBindings>
-->
<DockPanel LastChildFill="True">
<Canvas DockPanel.Dock="Top" Height="50"
Background="{StaticResource orangeGradientBrush2Stops}">
<Image Source="../Images/order.png" Width="40"
Height="40" Canvas.Left="5" Canvas.Top="5"/>
<Label Canvas.Left="50" Canvas.Top="10"
Width="auto" Height="auto" Content="EDIT ORDER"
FontSize="18" FontWeight="Bold"/>
</Canvas>
<DockPanel Margin="5"
DockPanel.Dock="Bottom" LastChildFill="True">
<StackPanel Orientation="Horizontal"
DockPanel.Dock="Bottom" Margin="6">
<Button x:Name="btnSave" Content="Save"
Height="auto" Width="auto" Margin="5"
FontFamily="Arial" Foreground="White"
Template="{StaticResource bordereredButtonTemplate}"
Command="{x:Static models:OrderViewModel.SubmitChangesCommand}" />
<Button x:Name="btnCancel" Content="Cancel"
Height="auto" Width="auto" Margin="5"
FontFamily="Arial" Foreground="White"
Template="{StaticResource bordereredButtonTemplate}"
Click="btnCancel_Click"/>
</StackPanel>
<ScrollViewer ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Auto"
DockPanel.Dock="Top">
-->
<validation:ErrorProvider x:Name="errorProvider">
<StackPanel Orientation="Vertical" Margin="5">
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="OrderID" />
<TextBox x:Name="txtOrderID"
Grid.Row="0" Grid.Column="1"
Margin="3"
Text="{Binding OrderID}"
HorizontalAlignment="Stretch"
IsReadOnly="True"/>
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ShipName" />
<TextBox x:Name="txtShipName"
Grid.Row="0" Grid.Column="1" Margin="3"
Text="{Binding ShipName, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource textStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,ElementName=txtOrderID}"
HorizontalAlignment="Stretch" />
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ShipAddress" />
<TextBox x:Name="txtShipAddress"
Grid.Row="0" Grid.Column="1" Margin="3"
Text="{Binding ShipAddress, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource textStyleTextBox}"
HorizontalAlignment="Stretch" Height="50"
MaxWidth="{Binding Path=ActualWidth,ElementName=txtOrderID}"
MinLines="1" MaxLines="2" />
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ShipCity" />
<TextBox x:Name="txtShipCity"
Grid.Row="0"
Grid.Column="1" Margin="3"
Text="{Binding ShipCity, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource textStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,ElementName=txtOrderID}"
HorizontalAlignment="Stretch" />
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ShipRegion" />
<TextBox x:Name="txtShipRegion"
Grid.Row="0"
Grid.Column="1" Margin="3"
Text="{Binding ShipRegion, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource textStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,ElementName=txtOrderID}"
HorizontalAlignment="Stretch" />
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ShipPostalCode" />
<TextBox x:Name="txtShipPostalCode"
Grid.Row="0" Grid.Column="1" Margin="3"
Text="{Binding ShipPostalCode, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource textStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,ElementName=txtOrderID}"
HorizontalAlignment="Stretch" />
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ShipCountry" />
<TextBox x:Name="txtShipCountry"
Grid.Row="0" Grid.Column="1" Margin="3"
Text="{Binding ShipCountry, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource textStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,ElementName=txtOrderID}"
HorizontalAlignment="Stretch" />
</Grid>
</StackPanel>
</validation:ErrorProvider>
</ScrollViewer>
</DockPanel>
</DockPanel>
</Window>
AboutWindow.xaml looks like:
Here is the layout for this Window (again, I've removed certain markup for clarity):
<Window x:Class="PhysicsHost.AboutWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
Title="Particles"
Icon="../Images/logo.png"
ResizeMode="NoResize"
Width="500" Height="350"
Background="#FF000000">
<Window.Resources>
<Storyboard x:Key="OnMouseEnterSachas">
....
....
</Storyboard>
<Storyboard x:Key="OnMouseEnterFredriks">
....
....
</Storyboard>
</Window.Resources>
<Window.Triggers>
....
....
</Window.Triggers>
-->
<DockPanel Width="Auto" Height="Auto"
LastChildFill="True" Background="#FF000000">
<Canvas Width="Auto" Height="49"
Background="#FFFF9900" DockPanel.Dock="Top">
<Image Width="200" Height="50"
Source="../Images/aboutHeader.png"/>
</Canvas>
<Grid Width="Auto" Height="Auto"
Background="#FF000000" DockPanel.Dock="Top">
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Canvas Grid.Row="0" Grid.Column="0"
Width="Auto" Background="White"
HorizontalAlignment="Stretch"
VerticalAlignment="Top" Height="25">
<Path x:Name="pthSachas" Fill="Black"
Stretch="Fill" Stroke="Black" Width="10"
Height="10" Data="M0,0 L 0,10 L 5,5"
Canvas.Left="20"
Canvas.Top="8" Visibility="Visible"/>
<Label x:Name="lblSachasBit" Width="133"
Height="auto" FontFamily="Aharoni"
Foreground="Black" Canvas.Left="31"
Content="What Sacha did"
Canvas.Top="4" />
<Path x:Name="pthFredriks" Fill="Black"
Stretch="Fill" Stroke="Black" Width="10"
Height="10" Data="M0,0 L 0,10 L 5,5"
Canvas.Left="239" Canvas.Top="8"
Visibility="Hidden"/>
<Label x:Name="lblFredriksBit"
Width="133" Height="auto"
FontFamily="Aharoni"
Foreground="Black" Canvas.Left="250"
Content="What Fredrik did"
Canvas.Top="4" />
</Canvas>
<Canvas Grid.Row="1" Grid.Column="0" >
-->
<TextBlock x:Name="tbSachas" Width="215"
Text="Sacha is responsible for converting
Fredriks Physics classes from a Winforms
environment into WPF. Sacha also created
this application, and the underlying classes
that support the application. Fredrik
and Sacha used to work together. Fredrik was
Sachas team leader. Sacha really wants
Fredrik to come and work at Sachas new job,
where they can share their love
of http://icanhascheezburger.com/"
TextWrapping="Wrap"
RenderTransformOrigin="0.5,0.5" Background="Black"
Canvas.Left="16" Foreground="#FFFFFFFF"
HorizontalAlignment="Left" Height="180"
VerticalAlignment="Stretch" Canvas.Top="0">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="23"/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
-->
<TextBlock x:Name="tbFredriks" Width="215"
Text="Fredrik is a Swedish chap that knows what's what when
it comes to programming. He used to be Sachas team leader,
but Sacha had to leave to pursue his WPF interest.
Fredrik can program anything (apart from WPF),
but is most happy writing games in
DirectX that he never finishes. He wrote the original
Physics for this application. Basically he's smart.
The best you'll ever meet. I once saw him write a 3D screen saver
in about 2 hours without needing to look anything up. He rocks"
TextWrapping="Wrap"
RenderTransformOrigin="0.5,0.5"
Background="Black"
Canvas.Left="250" Foreground="#FFFFFFFF"
HorizontalAlignment="Left" Height="1"
VerticalAlignment="Stretch" Canvas.Top="0">
<TextBlock.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="23"/>
</TransformGroup>
</TextBlock.RenderTransform>
</TextBlock>
</Canvas>
</Grid>
</DockPanel>
</Window>
ParticleCanvas - Advanced Layout
As ParticleCanvas
inherits from Canvas
, several layout oriented overrides must be performed. These are as follows:
ArrangeOverride
: When overridden in a derived class, positions child elements and determines a size for a FrameworkElement
derived class.
MeasureOverride
: When overridden in a derived class, measures the size in layout required for child elements and determines a size for the FrameworkElement
-derived class.
The ParticleCanvas
overrides these methods as follows:
protected override Size ArrangeOverride(Size arrangeSize)
{
foreach (UIElement element in base.InternalChildren)
{
double x;
double y;
double left = Canvas.GetLeft(element);
double top = Canvas.GetTop(element);
x = double.IsNaN(left) ? 0 : left;
y = double.IsNaN(top) ? 0 : top;
element.Arrange(new Rect(new Point(x, y), element.DesiredSize));
}
return arrangeSize;
}
protected override Size MeasureOverride(Size constraint)
{
Size size = new Size(double.PositiveInfinity, double.PositiveInfinity);
foreach (UIElement element in base.InternalChildren)
{
element.Measure(size);
}
return new Size();
}
Where each child is given as much space as they want.
You can refer to Part 2 for more information about Resources in WPF.
As with Layout, the demo app uses resources all over the place. Though, one thing I should point out is that they are all static resources. There are none that change once assigned, so there is no need for any DynamicResource allocation. I have partitioned most resources (there are still a few Window level resources around) into three files, as follows:
- StylesAndTemplatesCommon.xaml: Used by 1 or 2 common areas
- StylesAndTemplatesGlobal.xaml: Used by most demo application items
- StylesAndTemplatesValidation.xaml: Used for data validation purposes
Just to remind ourselves of how to use resources: we start by declaring a resource dictionary. Let's take the the StylesAndTemplatesValidation.xaml resource dictionary as an example.
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:PhysicsHost.ViewModel"
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
<!-- Resource dictionary entries should be defined here. -->
-->
<SolidColorBrush x:Key="SolidRedBrush" Color="Red" />
<SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />
-->
<Style x:Key="{x:Type ToolTip}" TargetType="ToolTip">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="HasDropShadow" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border Name="Border"
Background="{StaticResource SolidRedBrush}"
BorderBrush="{StaticResource SolidBorderBrush}"
BorderThickness="1"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<ContentPresenter
TextElement.Foreground="White"
Margin="4"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="HasDropShadow"
Value="true">
<Setter TargetName="Border"
Property="CornerRadius"
Value="4"/>
<Setter TargetName="Border"
Property="SnapsToDevicePixels"
Value="true"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
-->
<Style x:Key="validationStyleTextBox" TargetType="TextBox">
<Setter Property="Foreground" Value="#333333" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
</ResourceDictionary>
Then in the area where we want to use this resource, say on EditCustomWindow.xaml (where databinding is used), we simply need to reference the Resource Dictionary. I am using a MergedDictionary
, but there are other ways (like code). Let's see:
<Window x:Class="PhysicsHost.EditCustomerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:PhysicsHost.ViewModel"
xmlns:validation="clr-namespace:PaulStovell.Samples.WpfValidation"
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
Icon="../Images/logo.png"
Title="Particles" Height="360" Width="500"
ResizeMode="NoResize"
Background="Black"
TextElement.Foreground="White">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="../Resources/StylesAndTemplatesCommon.xaml"/>
<ResourceDictionary
Source="../Resources/StylesAndTemplatesValidation.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.CommandBindings>
...
...
</Window.CommandBindings>
<DockPanel LastChildFill="True">
...
...
<TextBlock Grid.Row="0" Grid.Column="0"
Text="ContactName" />
<TextBox x:Name="txtContactName"
Grid.Row="0" Grid.Column="1"
Margin="3"
Text="{Binding ContactName,
UpdateSourceTrigger=Explicit, ValidatesOnDataErrors=True}"
Style="{StaticResource validationStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,
ElementName=txtCustomerID}"
HorizontalAlignment="Stretch"/>
...
...
</DockPanel>
</DockPanel>
</Window>
We can see that there is a MergedDictionary
declared on the EditCustomWindow
Window, and that there is a TextBox
on this Window that uses a StaticResource
called "validationStyleTextBox
" which is using the resource whose Key
is "validationStyleTextBox
" from within the resource file that has been referenced through the use of the MergedDictionary
. This resource "validationStyleTextBox
" is contained within the StylesAndTemplatesValidation.xaml resource dictionary.
This is typically how the demo app is using resources. Though, there are occasions where I am using local Window or Control level resources, which don't use MergedDictionary
s. These are declared as follows:
<Window.Resources>
<Storyboard x:Key="OnMouseEnterSachas">
-->
<DoubleAnimation To="240" Storyboard.TargetName="tbSachas"
Storyboard.TargetProperty="(FrameworkElement.Height)" Duration="0:0:001"/>
<DoubleAnimation To="1" Storyboard.TargetName="tbFredriks"
Storyboard.TargetProperty="(FrameworkElement.Height)" Duration="0:0:001"/>
-->
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="pthFredriks"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:00"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Storyboard.TargetName="pthSachas"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:00"
Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
<Storyboard x:Key="OnMouseEnterFredriks">
-->
<DoubleAnimation To="240"
Storyboard.TargetName="tbFredriks"
Storyboard.TargetProperty="(FrameworkElement.Height)"
Duration="0:0:001"/>
<DoubleAnimation To="1"
Storyboard.TargetName="tbSachas"
Storyboard.TargetProperty="(FrameworkElement.Height)"
Duration="0:0:001"/>
-->
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="pthSachas"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:00"
Value="{x:Static Visibility.Hidden}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="pthFredriks"
Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame KeyTime="0:0:00"
Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</Window.Resources>
You can refer to Part 3 for more information about Commands and Events in WPF.
The demo application uses three Routed Commands, which are as follows:
CustomerViewModel
-> ShowHideOrdersForCustomerCommand
, for viewing the Orders for a specific Customer
CustomerViewModel
-> SubmitChangesCommand
, for saving a single Customer
OrderViewModel
-> SubmitChangesCommand
, for saving a single Order
The demo application uses the ModelView-ViewModel (MVVM) pattern, so these commands and how they tie up with the overall architecture will be mentioned a bit more in the Databinding section of this article. But for now, let's just concentrate on how the commands are defined and used.
The reason that Routed Commands are good is that we can have a layer of abstraction between the UI and where the command is actually declared. Of course, sometimes it is convenient and necessary to have the command declared/bound and executed all within the main UI; however, it is better to have some level of code separation. By separating the command from the UI, we can offer the potential for the UI VisualTree to be replaced. And as long as the new UI VisualTree contains the relevant command bindings, the application will still work. Josh Smith refers to this as structural skinning in his podder article series. Have a look. You'll see what I mean.
But anyway, back to the commands that this demo app declares. Let's look at them one by one.
CustomerViewModel: ShowHideOrdersForCustomerCommand
There is a command that is created within CustomerViewModel.cs that has its command CanExecute
/Executed
bindings set in the MainWindow
file. The actual UIElement
that triggers the Command is used on the PART_SHowHideOrders button within the CustomerUserControl
. As the CustomerUserControl
s are generated within the MainWindow
window, the correct command bindings exist, so when the button is pressed on the CustomerUserControl
button, due to the routed nature of Routed Commands, the notification flows back to the MainWindow
file which runs the Executed
method, which was set within the MainWindow
command bindings.
So we have some code to declare the command within the CustomerViewModel.cs file.
public static readonly RoutedCommand ShowHideOrdersForCustomerCommand
= new RoutedCommand("ShowHideOrdersForCustomerCommand", typeof(CustomerViewModel));
And then we have a StaticResource
which provides a default Style
for the CustomerUserControl
which contains the PART_SHowHideOrders button which uses this command. The command is actually set in code. This is shown below. I have removed anything that is not required to illustrate this point.
<!---->
<Style x:Key="defaultCustomerControlStyle"
TargetType="{x:Type local:CustomerUserControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type local:CustomerUserControl}">
.....
.....
<Button x:Name="PART_ShowHideOrders"
Template="{StaticResource bordereredButtonTemplate}"
Margin="5,0,0,0" Padding="4"
Width="auto" Height="auto"
HorizontalAlignment="Left"
FontFamily="Arial" FontSize="9"
Foreground="White" Content="Show My Orders"
VerticalAlignment="Center"/>
.....
.....
<ControlTemplate.Triggers>
.....
.....
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This is the code-behind that sets the PART_SHowHideOrders button command. By doing this in code behind, we are allowing the control to be lookless, where the end user can totally restyle the control. But we will talk more about this later in the Styles/Templates/Lookless Controls section.
PART_ShowHideOrders.Command = CustomerViewModel.ShowHideOrdersForCustomerCommand;
And the final part is the actual command bindings within the MainWindow.xaml file. Let's see that, starting with the XAML:
<Window.CommandBindings>
<CommandBinding Command="{x:Static models:CustomerViewModel.
ShowHideOrdersForCustomerCommand}"
CanExecute="ShowHideOrdersForCustomerCommand_CanExecute"
Executed="ShowHideOrdersForCustomerCommand_Executed"/>
</Window.CommandBindings>
And now the actual code-behind for the commands:
#region Command Sinks
private void ShowHideOrdersForCustomerCommand_CanExecute(
object sender, CanExecuteRoutedEventArgs e)
{
currentCustomerUserControl =
(e.OriginalSource as Button).Tag as CustomerUserControl;
if (currentCustomerUserControl != null)
{
currentCustomer =
currentCustomerUserControl.DataContext as Customer;
e.CanExecute =
customerViewModel.CustomerHasEnoughOrders(
currentCustomer.CustomerID);
}
else
e.CanExecute = false;
}
private void ShowHideOrdersForCustomerCommand_Executed(
object sender, ExecutedRoutedEventArgs e)
{
RemoveOrdersFromContainer();
foreach (Particle particle in
particleCanvasSimulation.ParticleSystem.Particles)
{
if (particle.Control.Equals(currentCustomerUserControl))
{
currentParticleForCustomer = particle;
break;
}
}
InitialiseOrders(currentCustomer.CustomerID,
currentParticleForCustomer);
}
#endregion
This command actually checks whether the current Customer
(LINQ to SQL object, more on this later) associated with CustomerUserControl.xaml has enough Order
s. If it does, the Show Orders button will be allowed to run; otherwise, it won't. See the button grayed out in one of the CustomerUserControl
s below.
When the command is run N-many (configurable in App.cs) Order
s associated with it are added to ParticleCanvas
as new Particle
s.
So that's how that command works.
CustomerViewModel: SubmitChangesCommand
There is also another command that is declared within the CustomerViewModel.cs that has its command CanExecute
binding set to be always true within the actual CustomerViewModel.cs file and the Executed
bindings set within the EditCustomerWindow.xaml window.
Let's see how this command is declared and has its CanExecute
binding to return true constantly. This is shown below, and is within the CustomerViewModel.cs file.
public static readonly RoutedCommand SubmitChangesCommand
= new RoutedCommand("SubmitChangesCommand", typeof(CustomerViewModel));
public CustomerViewModel()
{
CommandManager.RegisterClassCommandBinding(typeof(CustomerViewModel),
new CommandBinding(CustomerViewModel.SubmitChangesCommand,null,
delegate(object sender, CanExecuteRoutedEventArgs e) {
e.CanExecute = true;
}));
}
So that's that part. That just leaves the Executed
command binding. Which is within the EditCustomerWindow
Window, as shown below:.
<Window.CommandBindings>
<CommandBinding Command="{x:Static models:CustomerViewModel.SubmitChangesCommand}"
Executed="CustomerViewModelSubmitChangesCommand_Executed"/>
</Window.CommandBindings>
Now there must be a button that uses this command, and there is. It's the btnSave
button on the EditCustomerWindow
window, which is shown below:
<Window.CommandBindings>
<CommandBinding Command="{x:Static models:CustomerViewModel.SubmitChangesCommand}"
Executed="CustomerViewModelSubmitChangesCommand_Executed"/>
</Window.CommandBindings>
And the code-behind is as follows:
private void CustomerViewModelSubmitChangesCommand_Executed(
object sender, ExecutedRoutedEventArgs e)
{
try
{
if (UpdateBindings())
{
this.customerViewModel.SubmitChanges();
this.Close();
MessageBoxHelper.ShowMessageBox(
"Successfully updated Customer",
"Customer updated");
}
else
{
MessageBoxHelper.ShowErrorBox(
"Error updating Customer",
"Customer error");
}
}
catch (BindingException bex)
{
MessageBoxHelper.ShowErrorBox(
"Binding error occurred\r\n" + bex.Message,
"Binding error");
}
catch (Exception ex)
{
MessageBoxHelper.ShowErrorBox(
"An Error occurred trying to update the database\r\n"
+ ex.Message,"Database save error");
}
}
What is happening here is, within the CustomerViewModel.cs file, the command is declared and is set to always be enabled. And the Executed
command binding method simply updates the bound Customer
(LINQ to SQL, more on this later) object. And that's it for this command.
OrderViewModel: SubmitChangesCommand
Works in the same manner as the CustomerViewModel SubmitChangesCommand
command, but is used for saving Order
objects instead of Customer
objects.
You can refer to Part 4 for more information about Resources in WPF.
I didn't really have a cool idea about how to use DPs or Attached Properties for this article. So I did what any resourceful developer should do... steal some code, but quote the original source. To this end, I am using the code as posted by CodeProject user Rudi Grobler. Rudi's code uses Attached Properties (my favourite of all the DP family) to extend glass into your Window (which happens in Vista, by default) using P/Invoke. This technique is in a every WPF book out there, but the use of the Attached Property is the bit I like. And that is what Rudi did, so that's why I am using it.
Let's see how we do this. I have posted Rudi's original code here (I have just renamed the namespace). But you can get the full brief at his original article post, which is right here.
The important part is this class which provides the Attached Property and the P/Invoke stuff:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Runtime.InteropServices;
using System.Windows.Interop;
namespace PhysicsHost
{
[StructLayout(LayoutKind.Sequential)]
public struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
};
public class GlassEffect
{
[DllImport("DwmApi.dll")]
public static extern int
DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS pMarInset);
[DllImport("dwmapi.dll", PreserveSig = false)]
static extern bool DwmIsCompositionEnabled();
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled",
typeof(Boolean),
typeof(GlassEffect),
new FrameworkPropertyMetadata(OnIsEnabledChanged));
public static void SetIsEnabled(DependencyObject element, Boolean value)
{
element.SetValue(IsEnabledProperty, value);
}
public static Boolean GetIsEnabled(DependencyObject element)
{
return (Boolean)element.GetValue(IsEnabledProperty);
}
public static void OnIsEnabledChanged(DependencyObject obj,
DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue == true)
{
Window wnd = (Window)obj;
wnd.Loaded += new RoutedEventHandler(wnd_Loaded);
}
}
static void wnd_Loaded(object sender, RoutedEventArgs e)
{
Window wnd = (Window)sender;
Brush originalBackground = wnd.Background;
wnd.Background = Brushes.Transparent;
try
{
IntPtr mainWindowPtr = new WindowInteropHelper(wnd).Handle;
HwndSource mainWindowSrc = HwndSource.FromHwnd(mainWindowPtr);
mainWindowSrc.CompositionTarget.BackgroundColor =
Color.FromArgb(0, 0, 0, 0);
MARGINS margins = new MARGINS();
margins.cxLeftWidth = -1;
margins.cxRightWidth = -1;
margins.cyTopHeight = -1;
margins.cyBottomHeight = -1;
DwmExtendFrameIntoClientArea(mainWindowSrc.Handle, ref margins);
}
catch (DllNotFoundException)
{
wnd.Background = originalBackground;
}
}
}
}
This means we can now use this Attached Property using one line in one of our Windows, which will Glassify the Window, thanks to the Attached Properties and some P/Invoke magic.
<Window x:Class="PhysicsHost.EditOrderWindow"
....
....
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
....
....
</Window">
DPs and especially Attached Properties are the bomb. They are so good, the possibilities are endless.
You can refer to Part 5 for more information about DataBinding in WPF.
This is perhaps the most complicated part of the demo application. The reason that it's complicated is as follows:
- The demo app is using LINQ to SQL for the database interaction
- The demo app is using an N-Tier approach to working with the database
- The demo app is using validation through the .NET 3.5
IDataErrorInfo
interface
- The demo app is using manually updatable bindings (which was not discussed in Part 5)
As you can see, there is a fair bit to talk about just on this one subject. I think the best bet is to tackle this one thing at a time, in the order described above.
LINQ to SQL
As I am using SQL Server and VS2008, why not use LINQ to SQL I thought. So that's exactly what I dis. I used two tables, Customers/Orders, from the Northwind database. I just dragged these two tables onto the LINQ to SQL Designer.
This is all very standard stuff. This LINQ to SQL designer creates the file Northwind.Designer.cs which holds all the table mapping and objects required to communicate with these two Northwind tables. This class creates the NorthwindDataContext
, which is the main object that should be used to query/update the database against. As I say, this is all very standard.
The only issue was that I wanted to only have one instance of NorthwindDataContext
in the demo app. Luckily, due to the wonderful Partial Class feature, it was trivial to just create another partial class which provides the singleton instance for NorthwindDataContext
. This is shown below:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PhysicsHost.DataAccess
{
partial class NorthwindDataContext
{
#region Data
private static NorthwindDataContext instance = null;
private static readonly object padlock = new object();
#endregion
#region Singleton Instance
public static NorthwindDataContext Instance
{
get
{
lock (padlock)
{
if (instance == null)
{
instance = new NorthwindDataContext();
}
return instance;
}
}
}
#endregion
}
}
N-Tier approach
I wanted to make the demo application as easy to understand as I could, and I think a major part of this is structure. As such, the demo application uses an N-tier approach where the data flow is something like in the following diagram:
Where MainWindow
uses the CustomerViewModel
to fetch a number of Customer
objects which are then used as DataContext values for individual CustomerUserControl
objects. The Customer
objects are supplied by calling the CustomerBAL
object, which in turn gets the required Customer
s from the actual Northwind database by using the NorthwindDataContext
.
This may be a little clearer if we look at the code.
Starting at the top where the individual CustomerUserControl
objects are created, this is done within the MainWindow.Xaml.cs file.
private void InitialiseCustomers()
{
try
{
Customer[] custs = customerViewModel.GetCustomers().ToArray();
Particle[] particles = new Particle[custs.Count()];
int startPos = 100;
for (int i = 0; i < custs.Count(); i++)
{
if (i == 0)
{
particles[i] = new Particle(1.0f,
new Vector(startPos, startPos), true);
}
else
{
startPos += 200;
particles[i] = new Particle(1.0f,
new Vector(startPos, startPos), true);
}
particles[i].Control = getCustomerUserControl(custs[i]);
particleCanvasSimulation.ParticleSystem.Particles.Add(particles[i]);
}
anchor = new Particle(float.PositiveInfinity,
new Vector((double)(
this.particleCanvasSimulation.ActualWidth / 2), 40),true);
anchor.Control = getAnchorButton();
particleCanvasSimulation.ParticleSystem.Particles.Add(anchor);
GeneratePhysicsForParticles(particles,
anchor, false, 840.0f, 260.0f, 60.0f);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(ex.Message);
MessageBoxHelper.ShowMessageBox(ex.Message, "An error occurred");
}
}
private CustomerUserControl getCustomerUserControl(Customer cust)
{
CustomerUserControl control = new CustomerUserControl();
control.DataContext = cust;
particleCanvasSimulation.Children.Add(control);
control.PreviewMouseUp += new MouseButtonEventHandler(
particleCanvasSimulation.ParticleCanvas_PreviewMouseUp);
control.PreviewMouseMove += new MouseEventHandler(
particleCanvasSimulation.ParticleCanvas_PreviewMouseMove);
control.PreviewMouseDown += new MouseButtonEventHandler(
particleCanvasSimulation.ParticleCanvas_PreviewMouseDown);
control.MouseEnter += new MouseEventHandler(
particleCanvasSimulation.ParticleCanvas_MouseEnter);
Style defaultCustomerControlStyle =
this.TryFindResource("defaultCustomerControlStyle") as Style;
if (defaultCustomerControlStyle != null)
control.Style = defaultCustomerControlStyle;
return control;
}
There is a little bit of the Physics code in here (sorry), but Fredrik has explained that, so it should be clear enough I hope.
It can be seen that there is a call made to CustomerViewModel
to fetch all its Customer
objects; this is done using the GetCustomers()
method. Let's see the CustomerViewModel
class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;
using PhysicsHost.DataAccess;
namespace PhysicsHost.ViewModel
{
public class CustomerViewModel
{
#region Data
CustomerBAL customerBAL = new CustomerBAL();
#endregion
#region Commands
public static readonly RoutedCommand
ShowHideOrdersForCustomerCommand
= new RoutedCommand("ShowHideOrdersForCustomerCommand",
typeof(CustomerViewModel));
public static readonly RoutedCommand SubmitChangesCommand
= new RoutedCommand("SubmitChangesCommand",
typeof(CustomerViewModel));
#endregion
#region Ctor
public CustomerViewModel()
{
CommandManager.RegisterClassCommandBinding(
typeof(CustomerViewModel),
new CommandBinding(CustomerViewModel.SubmitChangesCommand,null,
delegate(object sender, CanExecuteRoutedEventArgs e) {
e.CanExecute = true;
}));
}
#endregion
#region Public Methods
public IEnumerable<Customer> GetCustomers()
{
return customerBAL.GetCustomers();
}
public bool CustomerHasEnoughOrders(string CustomerID)
{
return customerBAL.CustomerHasEnoughOrders(CustomerID);
}
public bool SubmitChanges()
{
return customerBAL.SubmitChanges();
}
#endregion
}
}
And it can be seen that this now calls the CustomerBAL
object. So examining that class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using System.Text;
using System.Runtime.CompilerServices;
namespace PhysicsHost.DataAccess
{
public class CustomerBAL
{
#region Ctor
public CustomerBAL()
{
}
#endregion
#region Public Methods
[MethodImpl(MethodImplOptions.Synchronized)]
public IEnumerable<Customer> GetCustomers()
{
int maxToShow =
int.Parse(App.Current.Properties["MAX_CUSTOMERS"].ToString());
NorthwindDataContext db = NorthwindDataContext.Instance;
var customers =
(from c in db.Customers where c.Orders.Count > 0 select c);
int maxExistingCustomerWithOrders = customers.Count();
int numOfCustomerToSelect = maxExistingCustomerWithOrders > maxToShow ?
maxToShow : maxExistingCustomerWithOrders;
return customers.Take(numOfCustomerToSelect).OrderBy(c => c.CustomerID);
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool CustomerHasEnoughOrders(string CustomerID)
{
int maxToShow =
int.Parse(App.Current.Properties["MAX_ORDERS"].ToString());
NorthwindDataContext db = NorthwindDataContext.Instance;
return (from o in db.Orders
where o.CustomerID ==
CustomerID select o).Count() > maxToShow;
}
[MethodImpl(MethodImplOptions.Synchronized)]
public bool SubmitChanges()
{
NorthwindDataContext db = NorthwindDataContext.Instance;
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
return true;
}
#endregion
}
}
This class is responsible for not only interacting with the NorthwindDataContext
, but also for running the business rules. In this case, the rules are very simple, does the customer has at least one associated Order
.
The final layer is the NorthwindDataContext
which, as I stated earlier, is standard LINQ to SQL generated code.
IDataErrorInfo Interface Validation
As we are dealing with bound data, when we edit either a Customer
or Order
object, we must ensure that there is a way to validate the data entered. I have chosen to use the new .NET 3.5 method which is by the use of the IDataErrorInfo
interface.
This interface must be used on the binding source object. In the demo app's case, this is either a Customer
or a Order
object coming from the auto generated LINQ to SQL code. When I first thought about this, I though this could be quite a pain as I would have to manually change auto generated classes. As luck would have it, Partial Classes to the rescue again. Looking at the auto generated stuff LINQ to SQL produces, say for Customer
, we can see the following:
[Table(Name="dbo.Customers")]
public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
{
....
....
....
}
Not only is the class partial (which means we can extend it in a different source code file... one of the best things MSFT ever did are Partial Classes), but the LINQ to SQL Designer makes all the generated classes implement the INotifyPropertyChanged
interface. Which is a must when dealing with WPF. This is excellent news. All that had to be done was to create an extra Partial Class for Customer
and one for Order
. This is shown below for Customer
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace PhysicsHost.DataAccess
{
public partial class Customer : IDataErrorInfo
{
#region Data
private StringBuilder combinedError = new StringBuilder(500);
#endregion
#region IDataErrorInfo Members
public string Error
{
get
{
return combinedError.ToString();
}
}
public string this[string columnName]
{
get
{
string result = null;
combinedError = new StringBuilder(500);
switch (columnName)
{
case "ContactName":
if (string.IsNullOrEmpty(this.ContactName))
{
result = "ContactName cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.ContactName) &&
this.ContactName.Length >= 15)
{
result = "ContactName should be <= 15 chars";
combinedError.Append(result + "\r\n");
}
break;
case "ContactTitle":
if (string.IsNullOrEmpty(this.ContactTitle))
{
result = "ContactTitle cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.ContactTitle) &&
this.ContactTitle.Length >= 15)
{
result = "ContactTitle should be <= 15 chars";
combinedError.Append(result + "\r\n");
}
break;
case "Address":
if (string.IsNullOrEmpty(this.Address))
{
result = "Address cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.Address) &&
this.Address.Length >= 30)
{
result = "Address should be <= 30 chars";
combinedError.Append(result + "\r\n");
}
break;
case "City":
if (string.IsNullOrEmpty(this.City))
{
result = "City cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.City) &&
this.City.Length >= 10)
{
result = "City should be <= 10 chars";
combinedError.Append(result + "\r\n");
}
break;
case "Region":
if (string.IsNullOrEmpty(this.Region))
{
result = "Region cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.Region) &&
this.Region.Length >= 15)
{
result = "Region should be <= 15 chars";
combinedError.Append(result + "\r\n");
}
break;
case "PostalCode":
if (string.IsNullOrEmpty(this.PostalCode))
{
result = "PostalCode cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.PostalCode) &&
this.PostalCode.Length > 10)
{
result = "PostalCode should be <= 10 chars";
combinedError.Append(result + "\r\n");
}
break;
case "Country":
if (string.IsNullOrEmpty(this.Country))
{
result = "Country cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.Country) &&
this.Country.Length > 10)
{
result = "Country should be <= 10 chars";
combinedError.Append(result + "\r\n");
}
break;
case "Phone":
if (string.IsNullOrEmpty(this.Phone))
{
result = "Phone cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.Phone) &&
this.Phone.Length > 15)
{
result = "Phone should be <= 15 chars";
combinedError.Append(result + "\r\n");
}
break;
case "Fax":
if (string.IsNullOrEmpty(this.Fax))
{
result = "Fax cant be empty";
combinedError.Append(result + "\r\n");
}
if (!string.IsNullOrEmpty(this.Fax) &&
this.Fax.Length > 15)
{
result = "Fax should be <= 15 chars";
combinedError.Append(result + "\r\n");
}
break;
}
return result;
}
}
#endregion
}
}
And now, we have all the bits we need to be able to perform validation using the information provided by the use of the IDataErrorInfo
interface. Basically, what we do is associate a Style
with a TextBox
which uses the error message generated by the IDataErrorInfo
interface implementation. This Style
is described a bit more below.
Manually Updatable Bindings
One of the things that I wanted was the ability for a user to be able to either update a bound field or to cancel the edit entirely. Within WPF, this is pretty easy to do; you just need to need to set the UpdateSourceTrigger
property of the binding to Explicit
. And then, you have to manually update the binding yourself in code. If we stick to just examining how the Customer
object works, the same is true for Order
objects.
There is a Window (EditCustomerWindw
) for editing a single instance of a Customer
object. Basically, the DataContext
of the EditCustomerWindw
is set to a single Customer
object, and then the EditCustomerWindw
has various markup that binds to this single Customer
object. But all updating to the bindings are set such that their UpdateSourceTrigger
properties are set to Explicit
. So there is some code that has to be done to update the underlying object from the bound values.
<Window x:Class="PhysicsHost.EditCustomerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:PhysicsHost.ViewModel"
xmlns:validation="clr-namespace:PaulStovell.Samples.WpfValidation"
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
Icon="../Images/logo.png"
Title="Particles" Height="360" Width="500"
ResizeMode="NoResize"
Background="Black" TextElement.Foreground="White">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="../Resources/StylesAndTemplatesCommon.xaml"/>
<ResourceDictionary
Source="../Resources/StylesAndTemplatesValidation.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding
Command="{x:Static models:CustomerViewModel.SubmitChangesCommand}"
Executed="CustomerViewModelSubmitChangesCommand_Executed"/>
</Window.CommandBindings>
<DockPanel LastChildFill="True">
<Canvas DockPanel.Dock="Top" Height="50"
Background="{StaticResource orangeGradientBrush2Stops}">
<Image Source="../Images/customer.png"
Width="40" Height="40"
Canvas.Left="5" Canvas.Top="5"/>
<Label Canvas.Left="50" Canvas.Top="10"
Width="auto" Height="auto"
Content="EDIT CUSTOMER"
FontSize="18" FontWeight="Bold"/>
</Canvas>
<DockPanel Margin="5"
DockPanel.Dock="Bottom" LastChildFill="True">
<StackPanel Orientation="Horizontal"
DockPanel.Dock="Bottom" Margin="6">
<Button x:Name="btnSave" Content="Save"
Height="auto" Width="auto" Margin="5"
FontFamily="Arial" Foreground="White"
Template="{StaticResource bordereredButtonTemplate}"
Command="{x:Static models:CustomerViewModel.
SubmitChangesCommand}" />
<Button x:Name="btnCancel"
Content="Cancel" Height="auto"
Width="auto" Margin="5"
FontFamily="Arial" Foreground="White"
Template="{StaticResource bordereredButtonTemplate}"
Click="btnCancel_Click"/>
</StackPanel>
<ScrollViewer
ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Auto"
DockPanel.Dock="Top">
<validation:ErrorProvider x:Name="errorProvider">
<StackPanel Orientation="Vertical" Margin="5">
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="CustomerID" />
<TextBox x:Name="txtCustomerID"
Grid.Row="0"
Grid.Column="1" Margin="3"
Text="{Binding CustomerID}"
HorizontalAlignment="Stretch"
IsReadOnly="True"/>
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ContactName" />
<TextBox x:Name="txtContactName"
Grid.Row="0"
Grid.Column="1" Margin="3"
Text="{Binding ContactName, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource validationStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,
ElementName=txtCustomerID}"
HorizontalAlignment="Stretch"/>
</Grid>
-->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0"
Grid.Column="0" Text="ContactTitle" />
<TextBox x:Name="txtContactTitle"
Grid.Row="0"
Grid.Column="1" Margin="3"
Text="{Binding ContactTitle, UpdateSourceTrigger=Explicit,
ValidatesOnDataErrors=True}"
Style="{StaticResource validationStyleTextBox}"
MaxWidth="{Binding Path=ActualWidth,
ElementName=txtCustomerID}"
HorizontalAlignment="Stretch"/>
</Grid>
.......
.......
.......
.......
</StackPanel>
</validation:ErrorProvider>
</ScrollViewer>
</DockPanel>
</DockPanel>
</Window>
And in the C# code-behind, there is the following code:
private bool UpdateBindings()
{
try
{
UpdateSingleBinding(txtContactName, TextBox.TextProperty);
UpdateSingleBinding(txtContactTitle, TextBox.TextProperty);
UpdateSingleBinding(txtAddress, TextBox.TextProperty);
UpdateSingleBinding(txtCity, TextBox.TextProperty);
UpdateSingleBinding(txtRegion, TextBox.TextProperty);
UpdateSingleBinding(txtPostalCode, TextBox.TextProperty);
UpdateSingleBinding(txtCountry, TextBox.TextProperty);
UpdateSingleBinding(txtPhone, TextBox.TextProperty);
UpdateSingleBinding(txtFax, TextBox.TextProperty);
return errorProvider.Validate();
}
catch
{
throw new BindingException(string.Format(
"There was a problem updating the Bindings for Customer {0}",
(this.DataContext as Customer).CustomerID));
}
}
private void UpdateSingleBinding(DependencyObject target, DependencyProperty dp)
{
BindingExpression bindingExpression =
BindingOperations.GetBindingExpression(
target, dp);
bindingExpression.UpdateSource();
}
Some more eagle eyed amongst you may notice that there is call to an object called errorProvider
. Well, just going back to the XAML for a second:
<Window x:Class="PhysicsHost.EditCustomerWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="clr-namespace:PhysicsHost.ViewModel"
xmlns:validation="clr-namespace:PaulStovell.Samples.WpfValidation"
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
Icon="../Images/logo.png"
Title="Particles" Height="360" Width="500"
ResizeMode="NoResize"
Background="Black" TextElement.Foreground="White">
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="../Resources/StylesAndTemplatesCommon.xaml"/>
<ResourceDictionary
Source="../Resources/StylesAndTemplatesValidation.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Window.Resources>
<Window.CommandBindings>
<CommandBinding
Command="{x:Static models:CustomerViewModel.SubmitChangesCommand}"
Executed="CustomerViewModelSubmitChangesCommand_Executed"/>
</Window.CommandBindings>
<DockPanel LastChildFill="True">
<Canvas DockPanel.Dock="Top" Height="50"
Background="{StaticResource orangeGradientBrush2Stops}">
<Image Source="../Images/customer.png"
Width="40" Height="40"
Canvas.Left="5" Canvas.Top="5"/>
<Label Canvas.Left="50" Canvas.Top="10"
Width="auto" Height="auto"
Content="EDIT CUSTOMER"
FontSize="18" FontWeight="Bold"/>
</Canvas>
<DockPanel Margin="5"
DockPanel.Dock="Bottom" LastChildFill="True">
<StackPanel Orientation="Horizontal"
DockPanel.Dock="Bottom" Margin="6">
<Button x:Name="btnSave" Content="Save"
Height="auto" Width="auto" Margin="5"
FontFamily="Arial" Foreground="White"
Template="{StaticResource bordereredButtonTemplate}"
Command="{x:Static models:CustomerViewModel.
SubmitChangesCommand}" />
<Button x:Name="btnCancel" Content="Cancel"
Height="auto" Width="auto" Margin="5"
FontFamily="Arial" Foreground="White"
Template="{StaticResource bordereredButtonTemplate}"
Click="btnCancel_Click"/>
</StackPanel>
<ScrollViewer ScrollViewer.HorizontalScrollBarVisibility="Hidden"
ScrollViewer.VerticalScrollBarVisibility="Auto"
DockPanel.Dock="Top">
<validation:ErrorProvider x:Name="errorProvider">
<StackPanel Orientation="Vertical" Margin="5">
......
......
</StackPanel>
</validation:ErrorProvider>
</ScrollViewer>
</DockPanel>
</DockPanel>
</Window>
Notice the use of the <validation:ErrorProvider x:Name="errorProvider">
element. This is a special element that I use to wrap my manually updatable bindings. As I am not setting the validation rules in the XAML and manually updating the bindings, I needed a way to create the validation rules at the point where the user chose to update the underlying data source. Basically, clicking the Save button. What this class does is use Reflection to get a list of the objects that have bindings, then get the DPs that it needs to use to update the bindings with, and then creates the appropriate validation rules for the bindings. It's cool. This comes from another MSFT MVP called Paul Stovell, and can be found right here.
Here is the relevant code for the validate()
method. Notice the use of the IDataErrorInfo
that was discussed earlier. See how it's all starting to fit together.
public bool Validate()
{
bool isValid = true;
_firstInvalidElement = null;
if (this.DataContext is IDataErrorInfo)
{
List<Binding> allKnownBindings = ClearInternal();
foreach (Binding knownBinding in allKnownBindings)
{
string errorMessage =
((IDataErrorInfo)this.DataContext)[knownBinding.Path.Path];
if (errorMessage != null && errorMessage.Length > 0)
{
isValid = false;
FindBindingsRecursively(
this.Parent,
delegate(FrameworkElement element, Binding binding,
DependencyProperty dp)
{
if (knownBinding.Path.Path == binding.Path.Path)
{
BindingExpression expression =
element.GetBindingExpression(dp);
ValidationError error = new
ValidationError(new ExceptionValidationRule(),
expression, errorMessage, null);
System.Windows.Controls.
Validation.MarkInvalid(expression, error);
if (_firstInvalidElement == null)
{
_firstInvalidElement = element;
}
return;
}
});
}
}
}
return isValid;
}
But I would recommend you go and read Paul's article for yourself; it's well thought out and described well.
You can refer to Part 6 for more information about Styles/Templates/Lookless Controls in WPF.
The demo application uses Styles/Templates all over the place. For instance, there are Styles/Templates for the following items:
Button
Tooltip
TextBox
with validation
MenuItem
CustomerUserControl
OrderUserControl
To truly understand these various Styles/Templates, I will include a screenshot of the Styled/Templated element and give a listing of the Style/Template that achieved the look in question.
Button ControlTemplates
<!---->
<ControlTemplate x:Key="anchorButtonTemplate"
TargetType="{x:Type Button}">
<ContentPresenter Margin="0"
Content="{TemplateBinding Content}"
Width="auto" Height="auto">
<ContentPresenter.ToolTip>
<StackPanel Margin="5,5,5,5"
Orientation="Vertical"
Background="Black">
<Label Height="auto"
FontSize="16" FontWeight="Bold"
Width="auto"
Background="Black" Foreground="White"
Content="Search Root"/>
<Label Height="auto" FontSize="10"
Width="auto" Foreground="White"
Content="Drag me around to see what happens"/>
</StackPanel>
</ContentPresenter.ToolTip>
</ContentPresenter>
</ControlTemplate>
<!---->
<ControlTemplate x:Key="bordereredButtonTemplate"
TargetType="{x:Type Button}">
<Border x:Name="border"
CornerRadius="3" Background="Transparent"
BorderBrush="White" BorderThickness="2"
Width="auto" Visibility="Visible">
<ContentPresenter Margin="3"
Content="{TemplateBinding Content}"
Width="auto" Height="auto"/>
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsEnabled" Value="false">
<Setter TargetName="border"
Property="Opacity" Value="0.4"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
Tooltip Style
<!---->
<Style TargetType="ToolTip">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="HasDropShadow" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border CornerRadius="10"
Background="{StaticResource orangeGradientBrush}"
BorderBrush="White" BorderThickness="1">
<ContentPresenter Width="auto" Height="auto" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Validation Styles
Starting with the textbox Style that is used to show a validation fault on a TextBox
:
<!---->
<Style x:Key="validationStyleTextBox" TargetType="TextBox">
<Setter Property="Foreground" Value="#333333" />
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},
Path=(Validation.Errors)[0].ErrorContent}"/>
</Trigger>
</Style.Triggers>
</Style>
And now the Validation Tooltip Style:
<!---->
<Style x:Key="{x:Type ToolTip}" TargetType="ToolTip">
<Setter Property="OverridesDefaultStyle" Value="true"/>
<Setter Property="HasDropShadow" Value="True"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ToolTip">
<Border Name="Border"
Background="{StaticResource SolidRedBrush}"
BorderBrush="{StaticResource SolidBorderBrush}"
BorderThickness="1"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}">
<ContentPresenter
TextElement.Foreground="White"
Margin="4"
HorizontalAlignment="Left"
VerticalAlignment="Top" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="HasDropShadow"
Value="true">
<Setter TargetName="Border"
Property="CornerRadius" Value="4"/>
<Setter TargetName="Border"
Property="SnapsToDevicePixels"
Value="true"/>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
MenuItem ControlTemplate
<!---->
<ControlTemplate x:Key="contentMenuItemTemplate"
TargetType="{x:Type MenuItem}">
<StackPanel Orientation="Horizontal">
<Image
Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type MenuItem}},Path=Tag}"
Width="25" Height="25" />
<Label
Content="{Binding RelativeSource={RelativeSource Mode=FindAncestor,
AncestorType={x:Type MenuItem}},Path=Header}"
Height="25" />
</StackPanel>
</ControlTemplate>
Lookless Controls
Recall in Part 6, I had a section about lookless controls. For those that haven't read that section, it's basically like this. Using Styles/Templates, a designer/developer is able to totally swap out the entire visual tree for a class. For example, I could have a UserControl that I want to contain a button and a listbox. But the designer or another programmer decides to create a new Style
for my UserControl that doesn't provide a button or a listbox.
How can we cope with that? Well, what we can do is decorate our objects with the PartTempateAttribue
to relay our intentions of what the control should contain within its visual tree in order for it to work correctly. And we can also check for the existence of the required visual elements that are required in order for the templated control to work.
This is what is done within the demo app for both CustomerUserControl
and the OrderUserControl
. Let's have a look at these, shall we?
CustomerUserControl
The CustomerUserControl
looks like this by default:
Now, this is down to a default Style
that I created, which tells the control what it's visual tree should be. This default Style
is shown below:
<!---->
<Style x:Key="defaultCustomerControlStyle"
TargetType="{x:Type local:CustomerUserControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type local:CustomerUserControl}">
<!---->
<Grid x:Name="LayoutRoot" Height="70"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Grid.RenderTransform>
<Border Margin="0,0,0,0"
Background="#FF303030" BorderBrush="#FFFFFFFF"
BorderThickness="2,2,2,2"
CornerRadius="3,3,3,3">
<DockPanel Width="Auto"
Height="Auto" LastChildFill="True">
<Border Height="20" DockPanel.Dock="Top"
Background="#FFFFFFFF"
CornerRadius="0,0,0,0" Margin="0,-2,0,0">
<Label Margin="45,-5,0,0"
Width="auto" Height="auto"
Content="{Binding Path=CustomerID}"
FontSize="14" FontWeight="Bold"/>
</Border>
<Canvas>
<Image Margin="-40,-40,0,0"
Width="50" Height="50"
Canvas.Left="18"
Canvas.Top="8"
Source="../images/customer.png"/>
<StackPanel Canvas.Left="0"
Canvas.Top="16"
Orientation="Vertical">
<StackPanel Orientation="Horizontal"
Margin="0,5,0,0">
<!---->
<Button x:Name="PART_ShowHideOrders"
Template="{StaticResource
bordereredButtonTemplate}"
Margin="5,0,0,0" Padding="4"
Width="auto" Height="auto"
HorizontalAlignment="Left"
FontFamily="Arial"
FontSize="9" Foreground="White"
Content="Show My Orders"
VerticalAlignment="Center"/>
<!---->
<Button x:Name="PART_Edit"
Template="{StaticResource
bordereredButtonTemplate}"
Margin="5,0,0,0" Padding="4"
Width="auto" Height="auto"
HorizontalAlignment="Left"
FontFamily="Arial"
FontSize="9" Foreground="White"
Content="Edit Me"
VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Canvas>
</DockPanel>
</Border>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter"
SourceName="LayoutRoot">
<BeginStoryboard
Storyboard="{StaticResource OnMouseEnterGrow}"/>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave"
SourceName="LayoutRoot">
<BeginStoryboard
Storyboard="{StaticResource OnMouseLeaveShrink}"/>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
Notice the use of some strange named elements:
<!---->
<Button x:Name="PART_ShowHideOrders"
Template="{StaticResource bordereredButtonTemplate}"
Margin="5,0,0,0" Padding="4"
Width="auto" Height="auto"
HorizontalAlignment="Left"
FontFamily="Arial" FontSize="9"
Foreground="White" Content="Show My Orders"
VerticalAlignment="Center"/>
<!---->
<Button x:Name="PART_Edit"
Template="{StaticResource bordereredButtonTemplate}"
Margin="5,0,0,0" Padding="4"
Width="auto" Height="auto"
HorizontalAlignment="Left"
FontFamily="Arial" FontSize="9"
Foreground="White" Content="Edit Me"
VerticalAlignment="Center"/>
These are the important parts that allow the control to work as it was intended to work. To understand this a bit better, examine the code-behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using PhysicsHost.DataAccess;
using PhysicsHost.ViewModel;
namespace PhysicsHost
{
[TemplatePart(Name = "PART_ShowHideOrders", Type = typeof(Button))]
[TemplatePart(Name = "PART_Edit", Type = typeof(Button))]
public partial class CustomerUserControl : UserControl
{
#region Ctor
public CustomerUserControl()
{
InitializeComponent();
}
#endregion
#region OnApplyTemplate
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Button PART_ShowHideOrders =
base.GetTemplateChild("PART_ShowHideOrders") as Button;
if (PART_ShowHideOrders != null)
{
PART_ShowHideOrders.Tag = this;
PART_ShowHideOrders.Command =
CustomerViewModel.ShowHideOrdersForCustomerCommand;
}
Button PART_EditButton =
base.GetTemplateChild("PART_Edit") as Button;
if (PART_EditButton != null)
{
PART_EditButton.Tag = this;
PART_EditButton.Click +=
new RoutedEventHandler(PART_EditButton_Click);
}
}
#endregion
#region Private Methods
private void PART_EditButton_Click(object sender, RoutedEventArgs e)
{
EditCustomerWindow ec = new EditCustomerWindow();
ec.DataContext = this.DataContext;
ec.Owner = MainWindow.GetWindow(this);
ec.ShowInTaskbar = false;
ec.WindowStartupLocation = WindowStartupLocation.CenterOwner;
ec.ShowDialog();
}
#endregion
}
}
Notice at the top of the class, I use the PartTempateAttribue
to relay my intentions of what elements were expected and what they should be called and what type of elements are expected.
The other thing to note is the OnApplyTemplate()
method, which is where the expected elements are looked for, and if found, have their required events wired up.
In the above example, I am also using a Command which is assigned to one of the expected buttons ("PART_ShowHideOrders"). The usage of Commands within the demo app was discussed in the Commands and Events section.
OrderUserControl
OrderUserControl
looks like this by default, and works in a very simliar manner to that described above:
<!---->
<Style x:Key="defaultOrderControlStyle"
TargetType="{x:Type local:OrderUserControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:OrderUserControl}">
<!---->
<Grid x:Name="LayoutRoot" Height="90"
RenderTransformOrigin="0.5,0.5">
<Grid.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform X="0" Y="0"/>
</TransformGroup>
</Grid.RenderTransform>
<Border Margin="0,0,0,0"
Background="#FF303030"
BorderBrush="#FFFFFFFF"
BorderThickness="2,2,2,2"
CornerRadius="3,3,3,3">
<DockPanel Width="Auto"
Height="Auto" LastChildFill="True">
<Border Height="20" DockPanel.Dock="Top"
Background="#FFFFFFFF"
CornerRadius="0,0,0,0" Margin="0,-2,0,0">
<Label Margin="45,-5,0,0"
Width="auto" Height="auto"
Content="{Binding Path=OrderID}"
FontSize="14" FontWeight="Bold"/>
</Border>
<Canvas>
<Image Margin="-40,-40,0,0"
Width="50" Height="50"
Canvas.Left="18"
Canvas.Top="8"
Source="../images/order.png"/>
<StackPanel Canvas.Left="0"
Canvas.Top="16"
Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Label Width="auto"
Height="auto" Content="Order Date:"
FontSize="9" FontWeight="Bold"
Foreground="#FFFFFFFF"/>
<Label Width="auto" Height="auto"
FontSize="9" Foreground="#FFFFFFFF"
Content="{Binding OrderDate,
Converter={StaticResource dateConv}}" />
</StackPanel>
<StackPanel Orientation="Horizontal"
Margin="0,5,0,0">
<!---->
<Button x:Name="PART_Edit"
Template="{StaticResource bordereredButtonTemplate}"
Margin="5,0,0,0" Padding="4"
Width="auto" Height="auto"
HorizontalAlignment="Left"
FontFamily="Arial"
FontSize="9" Foreground="White"
Content="Edit Me"
VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Canvas>
</DockPanel>
</Border>
</Grid>
<ControlTemplate.Triggers>
<EventTrigger RoutedEvent="Mouse.MouseEnter"
SourceName="LayoutRoot">
<BeginStoryboard
Storyboard="{StaticResource OnMouseEnterGrow}"/>
</EventTrigger>
<EventTrigger RoutedEvent="Mouse.MouseLeave"
SourceName="LayoutRoot">
<BeginStoryboard
Storyboard="{StaticResource OnMouseLeaveShrink}"/>
</EventTrigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
This time, there is only one required element that the control must have in order to work correctly:
<!---->
<Button x:Name="PART_Edit"
Template="{StaticResource bordereredButtonTemplate}"
Margin="5,0,0,0" Padding="4"
Width="auto" Height="auto"
HorizontalAlignment="Left"
FontFamily="Arial" FontSize="9"
Foreground="White" Content="Edit Me"
VerticalAlignment="Center"/>
And here is the code-behind:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using PhysicsHost.DataAccess;
namespace PhysicsHost
{
[TemplatePart(Name = "PART_Edit", Type = typeof(Button))]
public partial class OrderUserControl : UserControl
{
#region Ctor
public OrderUserControl()
{
InitializeComponent();
}
#endregion
#region OnApplyTemplate
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Button PART_EditButton =
base.GetTemplateChild("PART_Edit") as Button;
if (PART_EditButton != null)
{
PART_EditButton.Tag = this;
PART_EditButton.Click +=
new RoutedEventHandler(PART_EditButton_Click);
}
}
#endregion
#region Private Methods
private void PART_EditButton_Click(object sender, RoutedEventArgs e)
{
EditOrderWindow eo = new EditOrderWindow();
eo.DataContext = this.DataContext;
eo.Owner = MainWindow.GetWindow(this);
eo.ShowInTaskbar = false;
eo.WindowStartupLocation =
WindowStartupLocation.CenterOwner;
eo.ShowDialog();
}
#endregion
}
}
There are none, this is all the work of Sacha Barber and his old team leader Fredrik Bornander.
Various sources were used along the way; these were all included in the individual articles, so you should use those for a list of resources that pertain to the individual subject of the separate article. Links to the previous articles are here:
- Part 1: Layout
- Part 2: Resources
- Part 3: Commands and Events
- Part 4: Dependency Properties
- Part 5: DataBinding
- Part 6: Styles/Templates
- 23/03/08: Initial release.