玩转物理






4.94/5 (139投票s)
WPF:初学者指南系列,最终示例和物理学趣味。
序言与致谢
这是我的WPF初学者系列文章的最后一部分,对我来说,这真是一段不小的旅程。创作这个系列付出了相当多的努力。如果没有一些人的帮助,我无法完成它,特别是以下几位:
- Robert Ranck:为我所有的第一部分到第六部分的文章创作了VB.NET版本。这些文章构成了这个系列。
- Karl Shifflett:回答我们一些愚蠢的VB.NET问题,并纠正了我拼写错误和偶尔的语法错误。感谢你这双锐利的眼睛……你的眼睛没什么问题……你可能没睡够,但你的视力是一流的。别让任何人告诉你别的!
- Bea Costa:感谢你在第六部分允许我使用她的PlanetListbox。
- Paul Stovell:感谢他出色的WPF ErrorProvider类,当你手动绑定表达式更新时(就像我们在这里做的),它非常棒。
- Rubi Grobler:感谢《使用附加属性为WPF添加玻璃效果》这篇文章。以及我在本文中使用的代码。
引言
我一直在断断续续地研究这个应用程序,从第一部分开始。它已经变成了一种热爱。我想调整这个,调整那个。我终于对它满意了,真心希望你们大家和我一样喜欢它。我努力让它使用了我在这整个系列文章中涵盖的所有内容,这绝非易事,我告诉你。此外,我还想让它看起来很酷……因为我喜欢酷的东西。所以,自然而然,我选择了一个物理驱动的应用程序。酷毙了!!!
正如我所说,这篇文章是最后一部分(如果你愿意,可以称之为终极应用),我将使用我们一路学到的所有知识。提醒一下,这意味着我们将涵盖以下所有内容:
这篇文章实际上是我和我以前的团队领导的一次联合创作(这是我的第一次,但希望不是最后一次)。女士们先生们,请允许我介绍Fredrik Bornander先生。Fredrik不仅是我遇到的最棒的程序员,而且他是个很酷的人,我很喜欢他。我也喜欢和他交流想法。我们计划在这些领域和其他领域写更多文章,所以请关注。
然而,我们已经到了这一步,所以我想我将这样进行这篇文章:先谈谈应用程序的功能,给你们看一个视频,然后分解它(基本上是解剖它),并将分解后的应用程序的每个部分与原始文章系列中的一部分联系起来。这样,你就可以看到这个系列文章中哪一部分你需要回顾,如果你想更详细地了解某个功能是如何工作的。
由于这是一篇联合文章,Fredrik也创建了一些应用程序的代码,我也会提到Fredrik做了什么。事实上,我打算让Fredrik为他负责的部分写文字。当然,Fredrik是瑞典人,他的拼写需要检查(实际上,任何读过我文章的人可能都会觉得反过来……他可能需要检查我的拼写……尽管至少我可以说“元音”(vowels)而不是“鲸鱼”(whales),对吧,Fredrik……哈哈)。
有一件事我需要提一下,这次将不会发布VB.NET版本。这工作量太大了,我需要将注意力转移到其他文章上了。抱歉!
总之,以下是我们将在本文中涵盖的内容,但只有C#版本,再次抱歉。
演示应用程序的功能
本质上,演示应用程序非常简单。它使用标准的SQL Server Northwind数据库,首先检索一系列客户对象,然后在请求时,从数据库中获取客户相关的订单对象。客户和订单对象都允许用户编辑它们的详细信息,并进行一些验证以确保输入的数据有效。大致就是这样。但是,正如我们将看到的,这仍然为使用我们学到的所有WPF优点提供了足够的空间。正如我所说,我们还将加入一点物理学,让它以奇怪的方式移动……我们喜欢这样。
必备组件
如前所述,演示应用程序使用SQL Server(我使用的是SQL Server 2005),但只要你安装了Northwind数据库,无论使用哪个版本的SQL Server,应该都没问题。如果你没有Northwind数据库,你需要从这里下载并安装它。另外请注意,你需要修改应用程序使用的连接字符串,以匹配你自己的SQL Server安装。这可以在“PhysicsHost”项目中的相关app.config文件中完成。
<?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>
Sacha做了什么
我一直在研究这个应用程序,并将Fredrik的物理代码移植到WPF。它原来是WinForms的,所以Sacha为了把它WPF化做了必要的修改。但我不能为物理代码声称多少功劳。那是Fredrik的功劳。 Fredrik其实是一个不满意的游戏程序员,他每个月开始N个DirectX游戏,但没有一个完成。哈哈。至少他会和我一起完成这篇文章。所以,是的……我移植了物理代码到WPF,但也做了构成这个演示应用程序的所有其他WPF元素。这包括布局/资源/命令和事件/DP/数据绑定/样式和模板,以及LINQ to SQL。哦,我还创建了物理项目中的微不足道的DashedOutlineCanvas
。
Fredrik做了什么
Sacha以前和Fredrik一起工作,不久前在公司,Fredrik开始研究这个物理东西(尽管他应该在做他被支付的事情,即写无聊的Sybian C++,但嘿),这引起了Sacha的兴趣。这个Fredrik正在研究的物理东西后来变成了这个CodeProject文章。但Sacha认为这可以在WPF应用程序中使用,所以Sacha和Fredrik一起着手实现。结果,这个应用程序中看到的物理效果是基于Fredrik在他的原始CodeProject文章中做的原始物理效果。做得好,Fredrik。
演示应用程序的视频
鉴于物理学的性质,我在本文范围内能够充分展示附加演示应用程序的唯一方式是向您展示一个显示其运行情况的视频。因此,请点击下面的图片观看演示应用程序的视频:
![]() |
我建议等待视频完整流式传输后再观看。这样最有意义。 |
开始乐趣:解剖
好了,现在我告诉了你应用程序的功能,告诉你如何在家尝试,并向你展示了演示应用程序的视频。我将不详细介绍它是如何制作的。正如我所说,我认为最好的方法是解剖应用程序并将其与各个文章联系起来,这样如果你迷失了或者刚开始看这个系列,你可以回去查看相关的文章部分。
该应用程序结构分为两个项目:物理引擎和WPF应用程序。如下所示:
这两个项目将在下面详细讨论。WPF项目包含子文件夹,其中包含各种文件;文件夹名称会让你大致了解这些文件的用途。
物理
本节由Fredrik Bornander撰写,Sacha Barber校对并添加/插入。
通过使用简单的物理学来布局面板上的控件,可以很容易地使应用程序具有与使用“普通”静态布局的应用程序截然不同的感觉。
此物理实现的目标是为没有物理编程经验的开发人员提供一种简单的方法来构建酷炫的应用程序。
通过创建一个控件,在本例中是Canvas
的一个子类(称为ParticleCanvas
),它可以像窗口中的任何其他控件一样使用,从而可以轻松地将物理控制的控件添加到任何UI中。
添加到ParticleCanvas
的任何控件都可以与物理Particle
相关联;然后添加Spring
来约束粒子,以达到所需的任何配置。需要注意的是,控件应仅在代码隐藏中添加到ParticleCanvas
,在那里它们被分配给Particle
并附加到Spring
。如果在XAML中添加它们,仍然需要在代码隐藏中添加一些代码来将控件附加到Particle
s。
ParticleCanvas 类
ParticleCanvas
是拥有ParticleSystem
(物理系统)的画布控件。它使用DispatcherTimer
定期更新其内部ParticleSystem
(物理系统)的状态,以便进行动画处理。它通过告诉内部ParticleSystem
(物理系统)使用经过的时间进行积分来实现这一点;积分是计算ParticleSystem
(物理系统)下一个状态的操作。这每当定时器“滴答”一次时执行一次,每次积分后,所有与物理Particle
相关的控件都会重新定位到Particle
的新位置。这一切都由HandleWorldTimerTick
方法处理。
/// <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();
}
最后调用this.InvalidateVisual()
强制ParticleCanvas
重绘其控件,这将粒子“拉”到它们的新位置。
由于用户能够使用鼠标拖动受物理约束的控件,因此ParticleCanvas
必须跟踪一系列鼠标事件。
PreviewMouseDown
、PreviewMouseMove
和PreviewMouseUp
都用于处理控件拖动。
在PreviewMouseDown
期间,会查询物理Particle
列表,以查找与事件发送控件相关联的Particle
;也就是说,会搜索以确定当前是否有物理Particle
与接收鼠标事件的控件相关联。如果是这种情况,该控件会被移到Z轴最上面,使其显示在其他控件之上,并且相关Particle
的速度会被清除,质量设置为正无穷大。重置质量的原因是,在进行积分时,质量为无穷大的Particle
s会被ParticleSystem
(物理系统)忽略,这一点很重要,因为此时只有鼠标应该控制该Particle
的移动。
/// <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;
}
}
}
在PreviewMouseMove
期间,会测量鼠标光标移动的距离,并将相同的移动量更新到Particle
;控件本身将在下一个定时器滴答时自动更新其位置。
/// <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;
}
}
}
当鼠标释放并触发PreviewMouseUp
事件时,会恢复Particle
的原始质量;这将(如果原始质量不是正无穷大)允许ParticleSystem
(物理系统)再次移动粒子(因此也包括其相关的控件)。
/// <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;
}
}
}
ParticleCanvas
还(可选地)跟踪其所在的窗口移动时的情况,以便相应地更新Particle
s;这允许受Spring
s悬挂的控件自然行为,因为它们会摆回静止状态。ParticleCanvas
通过将所有可移动的(质量不等于正无穷大的)Particle
s移动窗口刚刚移动距离的负值来实现这一点。
这意味着,如果窗口向左移动了十个像素,粒子就会向右移动10个像素。自己试试看;看起来很酷。
/// <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 类
ParticleCanvas
的内部物理模拟由ParticleSystem
实例维护。ParticleSystem
是一个类,它拥有所有的Particle
s和Spring
s,保存所有的世界属性(如重力、阻尼),并计算积分。积分计算分为两个主要步骤:
步骤1:计算所有粒子的导数
这意味着计算速度和位置的差值,即加速度和速度。首先,当前作用在Particle
上的所有力都被清除,以便Particle
不受力的影响。然后应用“世界”力;这是重力和阻尼力,阻尼力类似于空气阻力。阻尼力对于简单的模拟(如这个)很重要,因为它对系统起到“阻尼”作用,使其稳定。之后,Spring
s施加到Particle
s上的力被应用到Particle
s上,之后,刚刚计算出的“状态”被存储在Particle
中作为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));
}
}
步骤2:用粒子状态更新位置
首先,Particle
状态按时间因子缩减,以便更新与已过去时间成比例。之后,通过简单地将状态速度添加到Particle
的当前速度来更新Particle
速度。位置也通常如此,但由于Particle
s可以被约束在ParticleCanvas
的矩形内,因此需要进行一些计算以确保它在屏幕边缘反弹。
/// <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;
}
}
ParticleSystem
类还提供了一个渲染Spring
的方法。
public void Render(System.Windows.Media.DrawingContext dc)
{
lock(Springs)
{
foreach (Spring spring in Springs)
{
spring.Render(dc);
}
}
}
Particle 类
ParticleSystem
类负责计算Particle
s的位置和速度。Particle
s是相当简单的类,它们在进行模拟时保存位置、速度、力和质量,以及可选地与Control
的关系。如果一个Control
与一个Particle
相关联,当调用SnapControl
方法时,该Control
的位置将与Particle
对齐。
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));
}
}
除此之外,Particle
本身不包含太多逻辑;所有逻辑都由ParticleSystem
类计算。如果给Particle
分配了正无穷大的质量,它就不会受到任何力的影响;这在创建模拟中的锚点时很有用。请注意,即使Particle
具有正无穷大的质量,它仍然可以通过鼠标拖动来移动。
Spring 类
Spring
s通过对Particle
s施加力来约束两个Particle
s,以满足Spring
的属性。Spring
的属性是:
- 静止长度:如果
Particle
s不受其他力影响,这是弹簧最终达到的距离。 - 弹簧常数:这是衡量弹簧有多硬的度量;值越高,“渴望”达到静止长度的程度越高。
- 阻尼常数:这是一个阻尼器,用于使弹簧移动得更慢并稳定模拟。
弹簧施加在其两个Particle
s上的力是通过一个可能看起来有点复杂但实际上很简单的方法计算出来的:
/// <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;
}
计算力的方向,这总是朝着另一个Particle
的方向;将Spring
常数应用于粒子之间距离与期望距离之间的差值。计算所需的阻尼量,这取决于粒子之间相互远离的程度;它们相互远离得越多,阻尼就越大。计算施加到第一个粒子的力,取负值,然后将其施加到第二个Particle
。
Spring
还可以使用ISpringRenderer
自行渲染;这只是一种让Spring
获得不同外观的方式。
我们如何使用布局(第一部分)
有关WPF中布局的更多信息,请参阅第一部分。
布局在演示应用程序中几乎无处不在。从窗口到用户控件,再到自定义样式/模板。它无处不在。也许最好的方法是选取几个窗口,提供截图和创建窗口的相应布局标记。不过,布局使用的区域太多了,无法一一介绍。但这应该能给你一个很好的概念。
演示应用程序中有四个窗口:
还有两个用户控件,尽管我暂时不讨论它们的布局,因为关于用户控件的讨论更适合在模板/样式和无外观控件部分进行。
如果我们选择其中两三个窗口,例如以下窗口,我认为我们应该可以讨论足够的布局了。
- MainWindow.xaml
- EditOrderWindow.xaml
- AboutWindow.xaml
好的,那么MainWindow.xaml看起来是这样的:
现在我们暂时忽略中间的ParticleCanvas
(黄色框区域),因为Fredrik应该在物理学部分讨论过它,我将在下面更详细地讨论布局方面。一旦我们忽略了ParticleCanvas
,布局就相当简单了(为清晰起见,我已删除部分标记)。
<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>
<!-- 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>
好的,那么EditOrderWindow.xaml看起来是这样的:
这是此窗口的布局(同样,为清晰起见,我已删除某些标记):
<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>
<!-- 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>
AboutWindow.xaml看起来是这样的:
这是此窗口的布局(同样,为清晰起见,我已删除某些标记):
<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>
<!-- 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 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>
<!-- 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 - 高级布局
由于ParticleCanvas
继承自Canvas
,因此必须执行几个面向布局的重写。它们如下:
ArrangeOverride
:在派生类中重写时,定位子元素并确定FrameworkElement
派生类的大小。MeasureOverride
:在派生类中重写时,测量子元素所需的布局大小,并确定FrameworkElement
派生类的大小。
ParticleCanvas
重写这些方法如下:
/// <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();
}
其中每个子项都被赋予了它们想要的尽可能大的空间。
我们如何使用资源(第二部分)
有关WPF中资源的更多信息,请参阅第二部分。
与布局一样,演示应用程序在各个地方都使用了资源。不过,我应该指出的一点是,它们全部是静态资源。一旦分配,它们就不会改变,所以不需要任何动态资源分配。我将大部分资源(仍有一些窗口级别的资源)划分为三个文件,如下所示:
- StylesAndTemplatesCommon.xaml:供1或2个通用区域使用
- StylesAndTemplatesGlobal.xaml:供大多数演示应用程序项使用
- StylesAndTemplatesValidation.xaml:用于数据验证目的
为了提醒自己如何使用资源:我们首先声明一个资源字典。让我们以StylesAndTemplatesValidation.xaml资源字典为例。
<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. -->
<!-- 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>
然后,在我们想使用此资源的区域,例如在EditCustomWindow.xaml(其中使用数据绑定)中,我们只需引用资源字典。我使用的是MergedDictionary
,但也有其他方法(如代码)。我们来看一下:
<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>
我们可以看到,在EditCustomWindow
窗口上声明了一个MergedDictionary
,并且该窗口上的一个TextBox
使用了名为“validationStyleTextBox
”的StaticResource
,该资源使用了通过使用MergedDictionary
引用的资源文件中的Key
为“validationStyleTextBox
”的资源。此资源“validationStyleTextBox
”包含在StylesAndTemplatesValidation.xaml资源字典中。
这通常是演示应用程序使用资源的方式。不过,有时我也会使用本地窗口或控件级别的资源,这些资源不使用MergedDictionary
。这些声明如下:
<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>
我们如何使用命令和事件(第三部分)
有关WPF中命令和事件的更多信息,请参阅第三部分。
演示应用程序使用三个路由命令,如下所示:
CustomerViewModel
->ShowHideOrdersForCustomerCommand
,用于查看特定客户的订单CustomerViewModel
->SubmitChangesCommand
,用于保存单个客户OrderViewModel
->SubmitChangesCommand
,用于保存单个订单
演示应用程序使用ModelView-ViewModel (MVVM)模式,因此这些命令以及它们如何与整体架构联系起来将在本文的数据绑定部分进行更多讨论。但现在,让我们只关注命令是如何定义和使用的。
路由命令之所以好,是因为我们可以在UI和命令实际声明的位置之间建立一个抽象层。当然,有时在主UI中声明/绑定和执行命令既方便又有必要;但是,最好有一定的代码分离。通过将命令与UI分离,我们可以提供替换UI VisualTree的可能性。只要新的UI VisualTree包含相关的命令绑定,应用程序仍然会工作。Josh Smith在他的podder文章系列中将其称为结构化皮肤化。看看吧。你就会明白我的意思了。
但总之,回到这个演示应用程序声明的命令。让我们逐个看看它们。
CustomerViewModel: ShowHideOrdersForCustomerCommand
在CustomerViewModel.cs中创建了一个命令,其命令CanExecute
/Executed
绑定在MainWindow
文件中设置。触发命令的实际UIElement
用于CustomerUserControl
中的PART_SHowHideOrders按钮。由于CustomerUserControl
是在MainWindow
窗口中生成的,因此存在正确的命令绑定,所以当按下CustomerUserControl
按钮时,由于路由命令的路由性质,通知会流回MainWindow
文件,该文件运行在MainWindow
命令绑定中设置的Executed
方法。
所以我们有一些代码在CustomerViewModel.cs文件中声明命令。
public static readonly RoutedCommand ShowHideOrdersForCustomerCommand
= new RoutedCommand("ShowHideOrdersForCustomerCommand", typeof(CustomerViewModel));
然后,我们有一个StaticResource
,它为包含使用此命令的PART_SHowHideOrders按钮的CustomerUserControl
提供了一个默认Style
。命令实际上是在代码中设置的。如下所示。我删除了任何不必要的内容以说明这一点。
<!-- 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>
这是设置PART_SHowHideOrders按钮命令的代码隐藏。通过在代码隐藏中这样做,我们允许控件是无外观的,终端用户可以完全重新设计控件的样式。但我们将在后面的样式/模板/无外观控件部分讨论更多。
PART_ShowHideOrders.Command = CustomerViewModel.ShowHideOrdersForCustomerCommand;
最后一部分是MainWindow.xaml文件中的实际命令绑定。让我们先看看XAML:
<Window.CommandBindings>
<CommandBinding Command="{x:Static models:CustomerViewModel.
ShowHideOrdersForCustomerCommand}"
CanExecute="ShowHideOrdersForCustomerCommand_CanExecute"
Executed="ShowHideOrdersForCustomerCommand_Executed"/>
</Window.CommandBindings>
现在是命令的实际代码隐藏:
#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);
}
#endregion
此命令实际上会检查当前与CustomerUserControl.xaml关联的Customer
(LINQ to SQL对象,稍后讨论)是否有足够的Order
s。如果有,则允许运行“显示订单”按钮;否则,则不允许。请看下面其中一个CustomerUserControl
中变灰的按钮。
当命令运行时,N个(可在App.cs中配置)相关Order
s作为新的Particle
s添加到ParticleCanvas
。
所以这就是那个命令的工作方式。
CustomerViewModel: SubmitChangesCommand
在CustomerViewModel.cs中还有一个命令,其命令CanExecute
绑定在实际的CustomerViewModel.cs文件中设置为始终为true,而Executed
绑定设置在EditCustomerWindow.xaml窗口中。
让我们看看这个命令是如何声明的,并且它的CanExecute
绑定始终返回true。如下所示,它位于CustomerViewModel.cs文件中。
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;
}));
}
所以这就是那部分。剩下的就是Executed
命令绑定。它在EditCustomerWindow
窗口中,如下所示:
<Window.CommandBindings>
<CommandBinding Command="{x:Static models:CustomerViewModel.SubmitChangesCommand}"
Executed="CustomerViewModelSubmitChangesCommand_Executed"/>
</Window.CommandBindings>
现在一定有一个按钮在使用这个命令,而且确实有。它是EditCustomerWindow
窗口上的btnSave
按钮,如下所示:
<Window.CommandBindings>
<CommandBinding Command="{x:Static models:CustomerViewModel.SubmitChangesCommand}"
Executed="CustomerViewModelSubmitChangesCommand_Executed"/>
</Window.CommandBindings>
代码隐藏如下:
/// <summary>
/// Sumbit the bound data changes back into the underlying
/// <see cref="Customer">Customer data object</see> and see
/// if we are able to update the Database
/// </summary>
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");
}
}
这里发生的是,在CustomerViewModel.cs文件中,命令被声明并设置为始终启用。Executed
命令绑定方法只是更新绑定的Customer
(LINQ to SQL,稍后讨论)对象。就这样,关于这个命令。
OrderViewModel: SubmitChangesCommand
其工作方式与CustomerViewModel SubmitChangesCommand
命令相同,但用于保存Order
对象而不是Customer
对象。
我们如何使用依赖属性(第四部分)
有关WPF中资源的更多信息,请参阅第四部分。
我并没有一个很好的想法如何为这篇文章使用DP或附加属性。所以,我做了任何有资源の開発者应该做的事情……窃取一些代码,但引用原始来源。为此,我使用了CodeProject用户Rudi Grobler发布的代码。Rudi的代码使用附加属性(我最喜欢的DP家族)将玻璃效果扩展到你的窗口(这在Vista中是默认的),使用P/Invoke。这项技术在所有的WPF书中都有介绍,但我喜欢附加属性的使用。而这正是Rudi所做的,所以这就是我使用它的原因。
让我们看看我们如何做到这一点。我已经在这里发布了Rudi的原始代码(我只是重命名了命名空间)。但你可以在他的原始文章页面上找到完整的介绍,就在这里。
重要部分是这个类,它提供了附加属性和P/Invoke内容:
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;
}
}
}
}
这意味着我们现在可以使用此附加属性,只需在一行代码中完成,就可以使我们的一个窗口“玻璃化”,这要归功于附加属性和一些P/Invoke魔法。
<Window x:Class="PhysicsHost.EditOrderWindow"
....
....
xmlns:local="clr-namespace:PhysicsHost"
local:GlassEffect.IsEnabled="true"
....
....
</Window">
DP,尤其是附加属性,太棒了。它们非常好,可能性是无限的。
我们如何使用数据绑定(第五部分)
有关WPF中数据绑定的更多信息,请参阅第五部分。
这也许是演示应用程序中最复杂的部分。复杂的原因如下:
- 演示应用程序使用LINQ to SQL进行数据库交互。
- 演示应用程序使用N层方法处理数据库。
- 演示应用程序通过.NET 3.5
IDataErrorInfo
接口进行验证。 - 演示应用程序使用手动可更新的绑定(这在第五部分中没有讨论)。
正如你所见,仅就这个主题而言,还有很多内容要讨论。我认为最好的办法是逐个解决这些问题,按照上述顺序。
LINQ to SQL
由于我使用的是SQL Server和VS2008,为什么不使用LINQ to SQL呢?所以我就是这么做的。我使用了Northwind数据库中的两个表,Customers/Orders。我只是将这两个表拖到了LINQ to SQL设计器上。
这一切都很标准。这个LINQ to SQL设计器创建了Northwind.Designer.cs文件,其中包含了与这两个Northwind表通信所需的所有表映射和对象。这个类创建了NorthwindDataContext
,这是查询/更新数据库时应该使用的主要对象。正如我所说,这一切都很标准。
唯一的问题是,我只想在演示应用程序中有一个NorthwindDataContext
实例。幸运的是,由于神奇的局部类(Partial Class)功能,只需创建一个另一个局部类来为NorthwindDataContext
提供单例实例就很容易了。如下所示:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace PhysicsHost.DataAccess
{
/// <summary>
/// This class is another part (partial) to the
/// NorthwindDataContext LINQ to SQL generated
/// classes. This part provides a thread safe
/// singleton of the NorthwindDataContext class
/// </summary>
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层方法
我想让演示应用程序尽可能容易理解,我认为结构是其中的一个重要部分。因此,演示应用程序采用N层方法,数据流如下面的图表所示:
其中MainWindow
使用CustomerViewModel
来获取一系列Customer
对象,然后这些对象被用作各个CustomerUserControl
对象的DataContext值。Customer
对象通过调用CustomerBAL
对象提供,而CustomerBAL
又通过使用NorthwindDataContext
从实际的Northwind数据库获取所需的Customer
s。
如果我们看看代码,这可能会更清楚一些。
从顶部开始,在创建各个CustomerUserControl
对象的代码中,这是在MainWindow.Xaml.cs文件中完成的。
/// <summary>
/// Create the Customer Particles
/// </summary>
private void InitialiseCustomers()
{
try
{
Customer[] custs = customerViewModel.GetCustomers().ToArray();
Particle[] particles = new Particle[custs.Count()];
int startPos = 100;
//setup Usercontrol particles
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]);
}
//setup anchor
anchor = new Particle(float.PositiveInfinity,
new Vector((double)(
this.particleCanvasSimulation.ActualWidth / 2), 40),true);
anchor.Control = getAnchorButton();
particleCanvasSimulation.ParticleSystem.Particles.Add(anchor);
//now generate the pyhsics for these new Particles
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;
}
这里有一点物理代码(抱歉),但 Fredrik已经解释过了,所以我希望它足够清楚。
可以看到,调用了CustomerViewModel
来获取它所有的Customer
对象;这是通过GetCustomers()
方法完成的。我们来看看CustomerViewModel
类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Input;
using PhysicsHost.DataAccess;
namespace PhysicsHost.ViewModel
{
/// <summary>
/// Custom View Model, used to bind to within the
/// <see cref="MainWindow">MainWindow</see>
/// </summary>
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
}
}
可以看到,它现在调用了CustomerBAL
对象。所以让我们检查一下那个类:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using System.Text;
using System.Runtime.CompilerServices;
namespace PhysicsHost.DataAccess
{
/// <summary>
/// Customer Business Layer
/// </summary>
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()
{
//As LINQ to SQL is dead clever we can simply call
//db.SubmitChanges, and as its been tracking changes automatically
//this is all we need to do
NorthwindDataContext db = NorthwindDataContext.Instance;
db.SubmitChanges(ConflictMode.FailOnFirstConflict);
return true;
}
#endregion
}
}
这个类负责与NorthwindDataContext
交互,也负责运行业务规则。在这种情况下,规则非常简单:客户是否至少有一个关联的Order
。
最后一层是NorthwindDataContext
,正如我之前所说的,这是标准的LINQ to SQL生成代码。
IDataErrorInfo接口验证
由于我们正在处理绑定数据,当我们编辑Customer
或Order
对象时,我们必须确保有一种方法来验证输入的数据。我选择使用新的.NET 3.5方法,即使用IDataErrorInfo
接口。
此接口必须用于绑定源对象。在演示应用程序的情况下,这是来自自动生成的LINQ to SQL代码的Customer
或Order
对象。当我第一次想到这一点时,我认为这可能很麻烦,因为我必须手动更改自动生成的类。运气好的话,局部类再次派上用场。看看LINQ to SQL生成的自动代码,比如Customer
,我们可以看到以下内容:
[Table(Name="dbo.Customers")]
public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged
{
....
....
....
}
不仅类是局部的(这意味着我们可以在另一个源文件***扩展它……MSFT做得最好的事情之一就是局部类),而且LINQ to SQL设计器使所有生成的类都实现了INotifyPropertyChanged
接口。这在处理WPF时是必须的。这是个好消息。所有需要做的就是为Customer
和Order
创建一个额外的局部类。如下所示的Customer
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
namespace PhysicsHost.DataAccess
{
/// <summary>
/// This class is another part (partial) to the
/// LINQ to SQL generated class within the
/// Northwind.designer.cs file. This class
/// provides the validation that is used
/// by the WPF databindings
/// </summary>
public partial class Customer : IDataErrorInfo
{
#region Data
private StringBuilder combinedError = new StringBuilder(500);
#endregion
#region IDataErrorInfo Members
/// <summary>
/// Return the full list of validation
/// errors for this object
/// </summary>
public string Error
{
get
{
return combinedError.ToString();
}
}
/// <summary>
/// Validates a particular column, and returns a
/// string representing the current error
/// </summary>
/// <param name="columnName">
/// The property name to validate</param>
/// <returns>A string representing the current error</returns>
public string this[string columnName]
{
get
{
string result = null;
combinedError = new StringBuilder(500);
//basically we need a case for each property you wish to validate
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
}
}
现在,我们拥有了能够使用IDataErrorInfo
接口提供的信息进行验证所需的所有部分。基本上,我们所做的是将一个Style
与一个TextBox
关联起来,该TextBox
使用IDataErrorInfo
接口实现生成的错误消息。这个Style
在下面稍作介绍。
手动可更新的绑定
我想到的一个功能是,用户既可以更新绑定字段,也可以完全取消编辑。在WPF中,这很容易做到;你只需要将绑定的UpdateSourceTrigger
属性设置为Explicit
。然后,你必须在代码中手动更新绑定。如果我们只关注Customer
对象的工作方式,对于Order
对象也是如此。
有一个窗口(EditCustomerWindw
)用于编辑单个Customer
对象实例。基本上,EditCustomerWindw
的DataContext
设置为一个单独的Customer
对象,然后EditCustomerWindw
有各种标记绑定到这个单独的Customer
对象。但是所有的更新绑定都设置为它们的UpdateSourceTrigger
属性为Explicit
。所以需要做一些代码来从绑定值更新底层对象。
<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">
<!--CustomerID-->
<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>
<!--ContactName-->
<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>
<!--ContactTitle-->
<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>
在C#代码隐藏中,有以下代码:
/// Update the changes back into the underlying
/// <see cref="Customer">Customer data object</see>
/// <returns>True if all binding values were valis. This
/// is done by using Paul Stovells <see cref="ErrorProvider">
/// ErrorProvider</see> class</returns>
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);
//now validate the binding values
return errorProvider.Validate();
}
catch
{
throw new BindingException(string.Format(
"There was a problem updating the Bindings for Customer {0}",
(this.DataContext as Customer).CustomerID));
}
}
/// <summary>
/// Updates a single TextBox binding
/// </summary>
private void UpdateSingleBinding(DependencyObject target, DependencyProperty dp)
{
BindingExpression bindingExpression =
BindingOperations.GetBindingExpression(
target, dp);
bindingExpression.UpdateSource();
}
一些更敏锐的读者可能会注意到有一个对名为errorProvider
的对象调用的。嗯,让我们再回到XAML一会儿:
<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>
请注意<validation:ErrorProvider x:Name="errorProvider">
元素的使用。这是一个我用来包装手动可更新绑定的特殊元素。由于我没有在XAML中设置验证规则并手动更新绑定,因此我需要在用户选择更新底层数据源的那个点创建验证规则。基本上,点击保存按钮。这个类通过反射获取具有绑定的对象列表,然后获取它需要的DP来更新绑定,然后为绑定创建适当的验证规则。这很酷。这来自另一位MSFT MVP,Paul Stovell,可以在这里找到。
这是validate()
方法相关的代码。注意前面讨论过的IDataErrorInfo
的使用。看看它是如何开始契合的。
/// <summary>
/// Validates all properties on the current data source.
/// </summary>
/// <returns>True if there are no errors displayed, otherwise false.</returns>
/// <remarks>
/// Note that only errors on properties that are displayed are included.
/// Other errors, such as errors for properties that are not displayed,
/// will not be validated by this method.
/// </remarks>
public bool Validate()
{
bool isValid = true;
_firstInvalidElement = null;
if (this.DataContext is IDataErrorInfo)
{
List<Binding> allKnownBindings = ClearInternal();
// Now show all errors
foreach (Binding knownBinding in allKnownBindings)
{
string errorMessage =
((IDataErrorInfo)this.DataContext)[knownBinding.Path.Path];
if (errorMessage != null && errorMessage.Length > 0)
{
isValid = false;
// Display the error on any elements bound to the property
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;
}
但我建议你去阅读Paul的文章,它构思巧妙,描述清晰。
我们如何使用样式/模板/无外观控件(第六部分)
有关WPF中样式/模板/无外观控件的更多信息,请参阅第六部分。
演示应用程序在各个地方都使用了样式/模板。例如,有以下项目的样式/模板:
Button
工具提示
- 带有验证的
TextBox
MenuItem
CustomerUserControl
OrderUserControl
为了真正理解这些各种样式/模板,我将包含一个样式/模板化元素的截图,并列出达到所需外观的样式/模板。
按钮控件模板
<!-- Anchor button template -->
<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>
<!-- Global Buttons -->
<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>
工具提示样式
<!-- Specialized 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>
验证样式
从用于在TextBox
上显示验证错误的文本框样式开始。
<!-- 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>
现在是验证工具提示样式。
<!-- 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>
MenuItem 控件模板
<!-- MenuItem -->
<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>
无外观控件
回想一下在第六部分中,我有一个关于无外观控件的部分。对于那些没有读过该部分的人来说,大致是这样的。使用样式/模板,设计师/开发人员可以完全替换一个类的整个视觉树。例如,我可能有一个UserControl,我想让它包含一个按钮和一个列表框。但是设计师或另一个程序员决定为我的UserControl创建一个新的Style
,它不提供按钮或列表框。
我们如何处理这种情况?嗯,我们可以用PartTempateAttribue
来装饰我们的对象,以传达我们的意图,即控件在其视觉树中应该包含什么才能正确工作。我们还可以检查必需视觉元素的可用性,以便模板化控件能够工作。
这就是在演示应用程序中为CustomerUserControl
和OrderUserControl
所做的。我们来看看它们,好吗?
CustomerUserControl
CustomerUserControl
默认看起来是这样的:
现在,这是由于我创建的一个默认Style
,它告诉控件它的视觉树应该是怎样的。这个默认Style
如下所示:
<!-- CustomerUserControl -->
<Style x:Key="defaultCustomerControlStyle"
TargetType="{x:Type local:CustomerUserControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate
TargetType="{x:Type local:CustomerUserControl}">
<!-- Control Layout -->
<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">
<!-- Show Orders Controls -->
<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"/>
<!-- Edit -->
<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>
请注意一些命名奇怪的元素的使用:
<!-- Show Orders Controls -->
<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"/>
<!-- Edit -->
<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"/>
这些是允许控件按预期工作的重要部分。要更好地理解这一点,请检查代码隐藏:
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
{
/// <summary>
/// Represents a Customer from the Northwind database.
/// This is lookless control, and as such as Style
/// can be applied, but there are expected to be 2
/// PARTs called
/// <list type="bullet">
/// <item>PART_ShowHideOrders, Button</item>
/// <item>PART_Edit, Button</item>
/// </list>
/// Which are required for the control to work correctly
/// </summary>
[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
/// <summary>
/// Find the required parts from the applied template
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//show hide button
Button PART_ShowHideOrders =
base.GetTemplateChild("PART_ShowHideOrders") as Button;
if (PART_ShowHideOrders != null)
{
PART_ShowHideOrders.Tag = this;
PART_ShowHideOrders.Command =
CustomerViewModel.ShowHideOrdersForCustomerCommand;
}
//edit button
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
/// <summary>
/// Show EditCustomerWindow
/// </summary>
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
}
}
注意在类顶部,我使用了PartTempateAttribue
来传达我的意图,即期望的元素是什么,它们应该被称为什么,以及期望的元素类型是什么。
另一件值得注意的事情是OnApplyTemplate()
方法,这是查找预期元素的地方,如果找到,则将它们的必需事件连接起来。
在上面的示例中,我还使用了一个命令,该命令被分配给其中一个预期的按钮(“PART_ShowHideOrders”)。命令在演示应用程序中的使用已在命令和事件部分讨论过。
OrderUserControl
OrderUserControl
默认看起来是这样的,并且其工作方式与上面描述的非常相似:
<!-- OrderUserControl -->
<Style x:Key="defaultOrderControlStyle"
TargetType="{x:Type local:OrderUserControl}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type local:OrderUserControl}">
<!-- Control Layout -->
<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">
<!-- Edit -->
<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>
这次,控件要正常工作,只需要一个必需的元素:
<!-- Edit -->
<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"/>
这是代码隐藏:
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
{
/// <summary>
/// Represents a Order from the Northwind database.
/// This is lookless control, and as such as Style
/// can be applied, but there are expected to be 1
/// PART called
/// <list type="bullet">
/// <item>PART_Edit, Button</item>
/// </list>
/// Which are required for the control to work correctly
/// </summary>
[TemplatePart(Name = "PART_Edit", Type = typeof(Button))]
public partial class OrderUserControl : UserControl
{
#region Ctor
public OrderUserControl()
{
InitializeComponent();
}
#endregion
#region OnApplyTemplate
/// <summary>
/// Find the required parts from the applied template
/// </summary>
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//edit button
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
/// <summary>
/// Show EditOrderWindow
/// </summary>
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
}
}
参考文献
没有了,这都是Sacha Barber和他以前的团队领导Fredrik Bornander的工作。
其他良好资源
一路使用了各种资源,这些资源都包含在各个文章中,所以你应该使用那些来获取与各篇文章独立主题相关的资源列表。以前文章的链接在这里:
历史
- 2008年3月23日:初始发布。