vendredi 11 avril 2008

Greater articles will you read

Note: This is an unedited contribution. If this article is inappropriate, needs attention or copies someone else's work without reference then please Report This Article

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 peoples 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 answered some of our dumb VB .NET queries, and for correcting my spelling and occassional syntax cockups...Thanks eagled eyed one...Nothing wrong with your may not sleep enough, but your eye sight is A1. Dont let anyone tell you different!
  • Bea Costa : For allowing me to use her PlanetListbox in Part6, thanks Bea
  • Paul Stovell : For his excellent WPF ErrorProvider class which is really cool when you are doing 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


I have been working on this app on and off since Part1 and its kind of become a labour of love. I wanted to tweek 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 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 him. 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 disect it) and relate each part of the disected 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 also a joint article, where Fredrik also created some of the applications 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 thats read any of my articles, its 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. Its too much work and I need to move my attention on to other articles now. Sorry

Any way here is what we are going to cover in this article but only in C#, sorry again.

What Does The Demo App Do

Essentially the demo app is very simple. It uses the standard SQL Server Northindwind SQL Server database. Where a number of Customer objects are first retrieved, and then when requested a Customers 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. Thats 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 you use. If you dont 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.

<?xml version="1.0" encoding="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> 

What Sacha Did

Sacha creates the entire application and ported Fredriks Physics code to WPF. It was WinForms based, so Sacha created the nessecary changes to it in order to WPFify it. But I can't really take much credit for the Physics code. That's Fredriks baby all the way. Fredrik is really a frustated games programmer that 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....Sacha 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 & Events/DPs/Databinding/Styles & Templates and also LINQ to SQL. Oh and Sacha also created the trivial DashedOutlineCanvas within the Physics project

What Fredrik Did

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 Sachas 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 orginal Physics stuff that Fredrik did for his orginal codeproject article. Nice one Fredrik.

A Video Of The Demo App

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

Click the image or here to view the video

I would suggest waiting for the ENTIRE video has streamed then watch it.

It will make most sense that way.

Let The Fun Begin : Disection

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. So now ill talk you thourgh how it was made. Like I say I think the best way is to disect the app and relate it back to the individual articles so that if you are lost or maybe new to this series, you can go back and have a look at the relevant article part

The application is structured using 2 projects. The Physics engine and the WPF application. This is shown below:

These 2 projects will be discussed in detail below. The WPF project has sub folders which contain various files, the folder name gives you an idea of what the files will be 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 it's controls.

The aim with this physics implementation was 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 sub class 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 controls added to that ParticleCanvas can then be related to a physics Particle, Springs 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 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 code behind to attached the controls to Particles

ParticleCanvas Class

The ParticleCanvas is the Canvas control that owns the 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 telling the internal ParticleSystem (Physics system) to do an integration using the elapsed time, an integration is the operation in which the ParticleSystems (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 Particles new position. This is all taken care of in the HandleWorldTimerTick method:

/// <summary> /// This method is hooked to a timer event and is responsible for calling /// the methods that updates the world state. /// </summary> private void HandleWorldTimerTick(object sender, EventArgs e) {     Rect constaintsRectancle = new Rect(0, 0, this.ActualWidth, this.ActualHeight);     lock (ParticleSystem.Particles)     {         // TODO: Make sure the actual timestep is configurable,          // or better yet, is the actual time elapsed between updates.         ParticleSystem.DoEulerStep(0.005f, constaintsRectancle);         foreach (Particle particle in ParticleSystem.Particles)         {             // Make sure the Controls are located center on their particles.             particle.SnapControl();         }     }     this.InvalidateVisual(); } 

The final call to this.InvalidateVisual() forces the ParticleCanvas to redraw its Controls which snaps the Particles to their new positions.

As the user is able to drag phyics constrained controls around using the mouse, the ParticleCanvas has to keep track of a series of mouse events.

PreviewMouseDown, PreviewMouseMove and PreviewMouseUp are all used to keep handle control dragging.

During PreviewMouseDown the list of physics Particles 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 phyics 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 ZIndex 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 Particles 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.

/// <summary> /// When the mouse is clicked on a control in the Simulation panel all /// Particles are searched to find the Particle that has the Control /// as associated control. Then that Particle is made immovable by setting /// its mass to infinity and are thus no longer effected by the simulation. /// Search for control has to go up through the tree as only the top level /// control is associated with the Particle if a particles associated  /// Control is a Panel containing more controls. /// </summary> 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 the 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.

/// <summary> /// This updates a Particles position when a Control is being dragged. /// Note that it is not required to move the Control as this is being  /// fixed in HandleWorldTimerTick when Controls snap to Particles /// positions. /// </summary> 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 Particles 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 it's related control) around.

/// <summary> /// If a particle is being dragged this event stops that and fires a  /// ParticleReleasedEvent to signal this. /// </summary> 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's contained in is being moved so that the Particles can be updated accordingly, this allows for a natural behaviour of the controls suspended by Springs 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) Particles by the negative distance the window just moved.

This means that if the window was dragged ten pixels to the left, the Particles would be relocated 10 pixels to the right. Try it for yourself it looks quite cool.

/// <summary> /// This method gets called whenever the parent form is moved and  /// moves the particles accordingly. /// </summary> 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's internal physics simulation is maintained by a ParticleSystem instance. The ParticleSystem is a class which owns all Particles and Springs, holds all world properties (such as gravity and drag) and computes the integration. The integraton 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 is puts a "damper" on the system which stabilizes it. After that the forces that the Springs add to the Particles are applied to the Particles and after that the just calculated "state" is stored away in the Particle as a ParticleState.

/// <summary> /// Calculates the derivative for the all entities in the simulation. /// </summary> public void CalculateDerivative() {     foreach (Particle particle in Particles)     {         // Clear all existing forces acting on the particle         particle.ResetForce();          // Add a gravity force         particle.AddForce(Gravity);          // Add world drag         Vector drag = particle.Velocity * -dragFactor;         particle.AddForce(drag);     }      foreach (Spring spring in Springs)     {         // Apply what ever forces this spring holds         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 propotional to the time elapsed. After that the Particle velocity is updated by simply adding the state velocity to the Particles current velocity. The same normally applies for position as well but as the Particles can be constrained to the ParticleCanvas's rectangle a few calculations have to be done to make sure it bounces of the edges if it is moving off the screen.

/// <summary> /// This method is called once per "frame" and is responsible for  /// calculating the next state of the simulation. That is the  /// velocities and positions for all the particles. /// </summary> /// <param name="deltaTime"></param> 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 the particle is supposed to be constrained to the canvas "visible" area         // do collision detection and figure out new position and velocity         if (particle.ConstrainedToCanvas &&                  !constaintsRectancle.Contains(newPosition.ToPoint()))         {             double x = particle.Velocity.X;             double y = particle.Velocity.Y;              // If particle is moving left and is to the left of the left canvas boundry             // clamp position and reverse velocity and damp velocity by wall friction             // This is repeated for each of the four borders.             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 Springs.

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 to calculating positions and velocities of Particles. Particles are rather simple classes that holds 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 Particles that Control's position is aligned with the Particles when the method SnapControl is called.

public void SnapControl() {     // If a Control is associated with this Particle then snap      // the Controls location so that it centers around the Particle     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 forces, 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

Springs are used to constrain two Particles by applying a forces to the Particles so that the Springs properties are satisfied. The properties of the Spring are:

  1. Rest length; This is the distance the spring will eventually end up in if the Particles are affected by no other forces.
  2. Spring Constant; This is a measure of how stiff the Spring is, the higher value the more "eager" to reach it's Rest Length.
  3. 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 it's two Particles are calculated by a method that might look a bit complicated but really is quite simple:

/// <summary> /// This method "applies" the springs forces by calculating the force /// and applying it to it's two <code>Particle</code>s. /// </summary> 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;      // FIXME: Should do something about zero-length springs here as the     // simulation will brake on zero-length springs...     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 Particles and the desired distance. Figure out the amount of damping needed, this is relative to how much or how little the Particles 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.

How We Used Layout (Part1)

You can refer to Part1 for more information about Layout in WPF

Layout is literally used everywhere within the demo app. From the Windows to the UserControls to the Custom Styles/Templates. Its every where. Probably the best way is to pick a couple of Windows and provide screen shots and the associated layout markup that creates the Windows. Though there are far to many areas where Layout is used to go into it every thing. But this should give you a good idea.

There are 4 WIndows, within the demo app

And there are 2 UserControls, though I wont 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 2 or 3 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 the MainWindow.xaml looks like:

Now if we foget about the ParticleCanvas in the middle (the yellow boxed area) just for the moment, as Fredrik should have talked about this a bit in the Physics section, and I will talk about the layout apects 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=""     xmlns:x=""     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>      <!-- START OF LAYOUT -->     <Grid x:Name="LayoutRoot" Background="Black">          <Grid.RowDefinitions>             <RowDefinition Height="*"/>             <RowDefinition Height="30"/>         </Grid.RowDefinitions>          <!-- Footer Banner -->         <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">             <!-- Top Banner -->             <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>             <!-- Particle Canvas -->             <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 the 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=""     xmlns:x=""     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>      <!-- START OF LAYOUT -->     <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">                 <!-- Paul Stovells Excellent ErrorProvider-->                 <validation:ErrorProvider x:Name="errorProvider">                                      <StackPanel Orientation="Vertical" Margin="5">                                                  <!--OrderID-->                         <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>                          <!--ShipName-->                         <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>                          <!--ShipAddress-->                         <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>                          <!--ShipCity-->                         <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>                          <!--ShipRegion-->                         <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>                          <!--ShipPostalCode-->                         <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>                          <!--ShipCountry-->                         <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> 

Ok so the 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=""     xmlns:x=""     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>      <!-- START OF LAYOUT -->     <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" >                 <!-- Sachas bit words -->                 <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"                  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>                 <!-- Fredriks bit words -->                 <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 - Advnaced Layout

As the ParticleCanvas inherits from Canvas seveal layout orientated 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:

/// <summary> /// Any custom Panel must override ArrangeOverride and MeasureOverride /// </summary>  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

How We Used Resources (Part2)

You can refer to Part2 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 3 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 resoutces. We start by declaring a resource dictionary. Lets take the the StylesAndTemplatesValidation.xaml resource dictionary as an example

<ResourceDictionary     xmlns=""      xmlns:x=""     xmlns:models="clr-namespace:PhysicsHost.ViewModel"      xmlns:local="clr-namespace:PhysicsHost"     local:GlassEffect.IsEnabled="true"          <!-- Resource dictionary entries should be defined here. -->      <!-- Brushes -->     <SolidColorBrush x:Key="SolidRedBrush" Color="Red" />     <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />      <!-- Exception/ValidationRule 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>        <!-- Exception/ValidationRule Based Validitaion TextBox 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 the 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). Lets see

<Window x:Class="PhysicsHost.EditCustomerWindow"     xmlns=""     xmlns:x=""     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 whos 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 dicitionary.

This is typically how the demo app is using resources. Though there are occassions where I am using local Window or Control level Resources, which dont use MergedDictionaries. These are declared as follows:

 <Window.Resources>      <Storyboard x:Key="OnMouseEnterSachas">         <!-- Expand Sachas text, and shrink Fredriks -->         <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"/>          <!-- Show Sachas arrow, and hide Fredriks -->         <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">         <!-- Expand Fredriks text, and shrink Sachas -->         <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"/>         <!-- Show Fredriks arrow, and hide Sachas -->         <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> 

How We Used Commands And Events (Part3)

You can refer to Part3 for more information about Commands And Events in WPF

The demo application is using 3 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 architecure will be mentioned a bit more in Databinding section of this article. But for now lets just concentrate 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 nesessary to have the command declared/bound and executed all within the main UI, however it is better to have some level of code seperation. By seperating 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. Youll see what I mean.

But anyway back to the commands that this demo app declares. Lets look at them one by one.

CustomerViewModel : ShowHideOrdersForCustomerCommand

There is a command that is created within the CustomerViewModel.cs that has its command CanExecute/Executed bindings set in the MainWindow file. And the actual UIElement that triggers the Command used on the PART_SHowHideOrders button within the CustomerUserControl. As the CustomerUserControls 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.cs 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.

<!-- CustomerUserControl --> <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> 

Code behind code thats sets the PART_SHowHideOrders button command. By doing this in code behind we are enabling allowing the control to be lookless, where the ens 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. Lets 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  /// <summary> /// Only allow the  /// <see cref="PhysicsHost.ViewModel. /// CustomerViewModel.ShowHideOrdersForCustomerCommand"> /// ShowHideOrdersForCustomerCommand </see>command to /// execute if the current Customer has enough orders /// to show /// </summary> 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; }  /// <summary> /// Shows Orders for selected Customer /// </summary> private void ShowHideOrdersForCustomerCommand_Executed(     object sender, ExecutedRoutedEventArgs e) {     //hide shown Customer Orders     RemoveOrdersFromContainer();     //show Orders for Customer selected     foreach (Particle particle in          particleCanvasSimulation.ParticleSystem.Particles)     {         if (particle.Control.Equals(currentCustomerUserControl))         {             currentParticleForCustomer = particle;             break;         }     }     //show orders for Customer     InitialiseOrders(currentCustomer.CustomerID,          currentParticleForCustomer); } 

Aucun commentaire: