使用 C# 4.5 构建高性能 WPF/E 企业级应用
深入参考如何在 WPF 中为 Windows 和 Web 构建面向性能的应用程序。
前言
本文提供了一份深入的参考,介绍了如何在 WPF 中为 Windows 和 Web 构建面向性能的应用程序。本文通过示例讨论了所有主要方面,包括设计模式的权衡、多线程、WCF 服务调用(客户端-服务器架构或 / 智能客户端模型)、内存管理、并行处理等,这些都是从头开始构建企业级应用程序的必备要素。
目录
- 概述。
- 引言。
- WPF/E 或 Silverlight
- 为 WPF/Silverlight 构建企业级应用程序需要考虑的因素
- WPF 编程模型。
- 初始架构。
- WPF 的 KSmart 模式。
- 模型层实现。
- 高级内存管理。
- 缓存对象。
- 弱引用。
- 垃圾回收。
- 非托管对象。
- 异步编程。
- Dispatcher。
- 使用 Dispatcher。
- 更新 UI。
- 异步更新 UI。
- 2. Background Worker (后台工作线程)。
- 在 WPF 中使用 Background Worker。
- 数据绑定。
- 数据流方向。
- 什么触发源更新?
- 页面和导航。
- 刷新当前页面。
- 导航生命周期。
- 提高性能的编程指南。
- 结论。
概述
本文提供了关于如何构建 WPF 应用程序的深入参考,这些应用程序面向高性能,并且仍然可以像 Silverlight 或任何其他第三方浏览器插件一样工作。本文还重点介绍了开发人员或架构师在选择设计模式、实现多线程或构建智能客户端应用程序时进行 WCF 服务调用时常犯的错误。它还讨论了导致内存占用过大、应用程序性能下降、分辨率问题、管理用户会话、并行处理等方面的关键设计问题。
此概念适用于 WPF、客户端运行的 WPF (XBAP)、Silverlight,以及所有遵循 WPF 概念的应用程序。
引言
如今,由于 WPF、XBAP 或 Silverlight 在应用程序构建方面提供了极大的灵活性,大量开发人员正在转向它们。现在,Microsoft 已停止对 .NET 4.0 之前的 Windows 控件进行更新,并完全专注于 WPF。这有助于构建当前行业所需的炫酷应用程序。Microsoft 为 WPF 4.5 或 Silverlight 5 添加了许多增强功能,有助于无缝构建跨 Windows 和 Web 的企业级应用程序,这些应用程序可以部署在 Windows/Web 上,几乎无需更改。
WPF/Silverlight 拥有大量很棒的功能,开发人员可以轻松调试 UI,创建更稳定的跨 Windows/Web 应用程序。
对于设计人员来说,理解框架、内存、并行处理和其他方面对于使用 WPF/E 或 Silverlight 构建高级应用程序至关重要。
WPF/E 或 Silverlight
WPF/E 指的是 Windows Presentation Foundation – Everywhere,Silverlight 也是 .NET 开发人员的下一代 UI 开发平台。Microsoft 正在统一开发工具,使得使用这些工具构建的应用程序可以针对桌面、笔记本电脑、触摸板、移动设备以及所有类型的设备,并具有与 WPF 相同的 UI。
在未来几年内,大多数在 Windows 操作系统上构建的、面向 Web 和 Windows 的应用程序都将使用 WPF 和 Silverlight。WPF 具有极其丰富的 UI,新的开发方式可能会使设计人员和开发人员面临复杂性,并带来内存泄漏、严重的性能陷阱以及巨大的内存消耗等问题。
当今世界,许多用户按数据量使用网络带宽,因此开发人员和设计人员更加关注较低的带宽消耗就显得尤为重要。如果未给予充分重视,这可能会成为一个琐碎的问题,并影响整个网络,使应用程序在企业中无法使用。
为 WPF/Silverlight 构建企业级应用程序需要考虑的因素
使用 WPF 构建企业级应用程序需要考虑多种因素。从一开始就理解 WPF 应用程序在 Windows 操作系统中的运行方式非常重要。
WPF 编程模型
WPF 编程在托管代码中进行。通过托管代码运行的 WPF 更加健壮、稳定,因为 CLR 提供了许多使开发富有成效的功能(包括内存管理、CTS 等),但这也需要付出一定的代价。
WPF 架构如下图所示
图中红色的部分是 WPF 的代码部分,包括 Presentation Framework、Presentation Core 和 milcore。这些是 WPF 的代码部分。其中 milcore 是一个非托管组件。它直接与 DirectX 引擎耦合;WPF 中的所有显示都通过 DirectX 引擎进行,这使得有效的硬件和软件渲染成为可能,并使应用程序看起来更炫酷。
初始架构
WPF 的架构考虑对于构建 WPF/Silverlight 应用程序至关重要,它决定了性能。可以构建比 Web 应用程序更健壮、比 ASP.NET 消耗更少带宽、内存占用更低并且可以利用硬件加速通过 Web 提供高质量图形的应用程序。以完全信任模式运行的 XBAP 应用程序可以在浏览器沙箱之外运行,并可以利用硬件,这在 ASP.NET 或任何其他需要单独安装在客户端的插件的 Web 技术中是不可能实现的。
由于 .NET Framework 随 Windows 操作系统一起发布,Windows Server 2003 托管 .NET Framework 1.1,Vista 托管 .NET 3.0,Windows 7 托管 .NET 3.5,Windows 8 和 Server 2008 托管 .NET 4.0,因此所有应用程序都可能需要 .NET Framework,因为它是构建 Windows 上下一代应用程序的唯一模型。
Windows 8 支持 Metro 风格的应用程序,这些应用程序非常强大,能够提供高端图形、高质量 UI,并且可以运行在 Windows、Web、触摸板、笔记本电脑、手机等各种平台上,使用 WPF 构建应用程序时,只需进行相同或略微的代码更改,这使其成为所有开发人员的理想平台。
考虑到初始架构,WPF 提供了各种编程模型,如 MVVM(Model View ViewModel)是最著名的,MEF(Managed Extensibility Framework)、PRISM(嵌入 MVVM 设计)、MVP 等。
MVVM 是基于其前身 MVP(Model View Presenter)、MVC(Model View Controller)等其他设计模式而创造的。MVC 是在 Ruby on Rails 开发中创造的一种设计模式,它变得非常流行,并在 ASP.NET 应用程序中得到了广泛的应用,为开发提供了新的平台。
注意:我想在此强调的主要观点是这些设计模式,虽然许多网站指出这些设计模式在构建企业级应用程序时提供了巨大的好处,但大多数设计人员或开发人员倾向于遵循这些模式,因为大多数 WPF 应用程序都属于 MVVM 或 PRISM,少数基于 MVC、MVP 或 MEF 框架构建。
这些架构的详细描述超出了本文的范围。根据我在 6 家公司开发超过 20 个应用程序、服务约 15+ 客户的经验,我认为这些架构使开发非常繁琐,并在开发中导致各种缺陷,可能导致需要专家级开发人员才能实现的各种问题,例如实现不必要的命令、传递事件参数给函数变得繁琐、降低性能、增加内存消耗以及增加网络带宽。
然而,我并不是反对它们,它们仅在特定类型的开发中需要,而不应成为开发人员和设计人员的常规选择。
它们增加了开发中的大量额外复杂性,而非解决问题,甚至可能导致生产环境中的应用程序无法使用。
MVVM 具有以下架构:
开发人员通常会选择使用 PRISM 设计模式,它在应用程序开发中内置了 MVVM。这种架构具有视图层,通常是表示层或 UI。PRISM 或 MVVM 设计模式表明 XAML 文件将没有 .cs 文件。
这对于开发人员来说可能更加繁琐,因为将没有 .xaml.cs 文件,所有事件都必须作为方法推送到 ViewModel,并且对于需要复杂参数的事件,开发人员会觉得非常困难,特别是那些从 ASP.NET 迁移过来的开发人员,并且没有任何额外的优势。当 UI 开发人员在 Expression Blend 中开发并在 .NET 中由开发人员使用时,这可能很有帮助。然而,这增加了额外的复杂性,而大多数项目都需要开发人员参与 UI 开发,这对于他们来说会非常繁琐。
可以附加 ICommand 对象而不是委托命令,并且可以在没有 PRISM 或 MVVM 的情况下完成。
MVVM 架构期望所有从 Model 到 Service 的调用都采用异步模式。这可能看起来有益,但它增加了应用程序开发的巨大开销和复杂性。由于服务调用是异步的,因此可以一次触发多个服务调用,但结果需要在 Completed 事件中捕获。(在正常配置下,这仅支持远程服务的 CallBack 功能)。
例如,假设您正在从数据库填充员工历史记录,该数据库有多个服务调用。在所有服务调用完成之前,UI 应该被阻止。在这种情况下,使用 CallBack 方式的异步编程非常难以管理。
下面演示的 Calculator 服务代表了这种情况。
public void Add()
{
MathClient.AddAsync();
}
Public void Sub()
{
MathClient.SubAsync();
}
public void Add_Completed(object sender, EventArgs e)
{
//Process Result
}
Public void Sub_Completed(object sender, EventArgs e)
{
//Process Result
}
在上面的代码中,哪个先返回结果,哪个后返回结果,开发人员无法预测,在所有结果返回之前,很难阻止 UI 线程。很难同步所有项目。非常难以处理服务。服务有内部的 Channel,这是非托管的,需要释放,否则内存消耗会很高。我们经常看到 Silverlight 或 WPF 应用程序挂起 UI 的情况,这使得应用程序在生产环境中非常不可行。
必须以同步方式进行服务调用以避免所有这些问题。
由于几乎所有的代码都包含委托和事件,它会消耗更多内存,处理时间更长,并且由于这里所有内容都是异步的,它可能导致代码不稳定,需要解决。
有可能会多次调用同一服务,导致不必要的服务器往返,还可能影响带宽,您会发现仅数据传输就比 Web 应用程序消耗更多的内存,性能也会受到影响,导致 UI 更新时间长。
WPF 的 KSmart 模式
在此,我提出了我的自定义设计模式,它通过简化应用程序开发、保持低内存占用、提高稳定性、提高性能、减少网络带宽来解决 WPF 开发中遇到的大部分问题。
KSmart 模式的详细信息将在另一篇 Code Project 文章中单独详细介绍,其中包含各种设计模式与 KSmart 模式的比较。
Ksmart 模式具有视图,通常包含 UI 的表示逻辑。控制器可以是 .xaml.cs,也可以是 ViewModel,或两者的组合,它包含数据绑定、事件处理等的代码。所有处理逻辑都将由业务层完成,并且该层可以在不同的控制器之间共享。Model 包含进行服务调用、使用后释放服务的代码。服务包含 Model 调用所用的 WCF 服务。服务端的业务逻辑由业务层处理。DAL 会调用数据库并将结果返回给业务层。
服务接口和数据契约项目将跨所有层共享,并具有类型转换为服务、在另一层释放服务的灵活性,并且数据通过应用程序中的数据契约进行共享。
此设计模式在实现 WPF/E 或 Silverlight 的有效代码方面提供了更大的灵活性。
模型层实现
即使模型层向 WCF 客户端提供服务接口,它仍将是提高性能、提供更好的内存管理、减少网络带宽的关键组件。
让我们看看模型层如何提高应用程序的整体性能。模型层调用 WCF 层。模型层可以对 WCF 中的一组操作进行异步调用,这些操作的结果需要并行处理。模型层可以等到所有操作都返回结果,从而提高应用程序的性能,并且仍然以同步方式将结果返回给业务层。
它负责更好的内存管理,内存必须在使用非托管通道进行通信后释放。一旦返回结果,服务就应该被释放。
public class CustomerModel
{
public int Add()
{
using (CalcService service = new CalcService())
{
return service.Add();
}
}
public int Sub()
{
using (CalcService service = new CalcService())
{
return service.Sub();
}
}
}
在上面的代码中,创建了服务,进行了服务调用,并释放了服务。这类代码可以大大减少内存占用,使 WPF/Silverlight 应用程序更健康、性能更好,这在 WCF 中是必需的。
CalcService 是一个包装器,它负责打开通道并返回服务。
这对于应用程序性能、更好的内存管理和减少网络带宽至关重要。与
public void Add()
{
MathClient.AddAsync();
}
Public void Sub()
{
MathClient.SubAsync();
}
public void Add_Completed(object sender, EventArgs e)
{
//Process Result
}
Public void Sub_Completed(object sender, EventArgs e)
{
//Process Result
}
在这里,很难释放对象,很难在对象之间保持同步,消耗更多内存、带宽并阻碍性能,因为所有内容都基于委托和事件。
高级内存管理
缓存对象
将所有对象(如 ViewModel 对象、Model 对象、业务对象、静态对象等)保存在一个集中的缓存中。释放集中缓存可以大大减少内存占用。
Microsoft 在 .NET 运行时中提供了内置缓存,但我强烈建议不要使用它,因为如果发生异常,缓存会自动失效。此外,在会话超时、应用程序登出、重新加载新控件等时,很难删除所有缓存项。缓存必须在所有这些时间被清除。
自定义的轻量级、特定于应用程序的缓存如下所示:
public class ApplicationCache
{
Dictionary<string, object> CentralCache = new Dictionary<string, object>();
public void Add(string key, object Value)
{
if (!CentralCache.ContainsKey(key))
{
CentralCache.Add(key, Value);
}
}
public object GetItem(string key)
{
if (CentralCache.ContainsKey(key))
{
return CentralCache[key];
}
return null;
}
static ApplicationCache cache = null;
public static ApplicationCache CreateInstance()
{
if(cache == null)
{
return new ApplicationCache();
}
return cache;
}
}
在此,将 null 赋给缓存会使整个缓存失效,并且程序员可以更好地控制缓存,并且在异常期间不会自动失效。
像弱引用一样使用此缓存。例如,将对象保留在弱引用中,将对象保留在此缓存中,从而提供更多好处。
弱引用
避免将对象保留在弱引用中,因为对象随时可能被垃圾回收器移除,引用会自动销毁。
垃圾回收
理解垃圾回收的工作原理对于构建高性能、低内存占用的应用程序至关重要,这些应用程序在生产环境中表现良好。所有 .NET 中的托管对象都保存在托管堆中,没有关于托管堆大小的信息,也没有关于何时触发垃圾回收的信息。最好定期触发垃圾回收,或者在进行导航更改、应用程序登出、会话超时等操作时触发。
垃圾回收发生在两种类型的对象上:
强引用对象,默认情况下所有对象都是强引用,垃圾回收器需要显式标记对象以进行收集。
ApplicationCache cache = new ApplicationCache();
object item = cache.GetItem("itemKey");
cache = null;
GC.Collect();
弱引用对象,它们会被垃圾回收器自动拾取。这些对象不需要显式标记,并且它们是短暂的对象。
class Program
{
static void Main(string[] args)
{
WeakReference refe = new WeakReference(new Test());
//test.Display("123");
GC.Collect();
//test.Display("123");
Console.WriteLine(refe.IsAlive);
Console.Read();
}
}
在上面的代码中,`refe.IsAlive` 打印为 False,因为对象已被垃圾回收器清除。需要在此之间找到适当的权衡才能实现解决方案。
非托管对象
执行操作且不包含托管代码的对象,如 FileStream 对象、DataSet、DataTable、COM 对象、WCF 服务对象、Channel 对象、SQL 连接对象、Command 对象、Windows GDI 对象等,需要手动 Dispose 这些对象以进行内存清理。
重要的是要理解,这些非托管对象会消耗大量内存,忽略 Dispose 这些对象会导致生产环境中出现严重的内存泄漏,用户在开发过程中也可能遇到此问题。
上面提到的所有对象(ComObjects 除外)都有 `Dipose()` 接口,可以在使用后调用。可以使用 `using` 块,并且推荐用于此类场景。以下代码对此进行了演示:
class Program
{
static void Main(string[] args)
{
DataSet ds = null;
using (SqlConnection conn = new SqlConnection())
{
//Open the connection
using (SqlCommand comm = new SqlCommand())
{
//Command code here
//Excecute the command
}
ds.Dispose();
}
}
}
在上面的代码中,所有非托管代码(如连接、命令)都嵌入在 `using` 块中,并在超出作用域后自动释放。`ds` 类型为 `DataSet`,通过调用 `dispose` 接口进行释放。
COM 对象可以按如下方式释放:
while (Marshal.ReleaseComObject(comObject) > 0){}
COM 对象被包装在 Callable Wrapper 中。Runtime Callable Wrapper 每次 COM 接口指向它时都有一个引用计数。**ReleaseComObject 方法**会递减运行时可调用包装器的引用计数,如果存在多个引用,则此方法会递减并返回剩余引用的数量。
为了确保运行时可调用包装器和原始 COM 对象被释放,应该构造一个循环,直到返回的引用计数为零。
异步编程
异步编程既有优点也有缺点。但是,如果我们正确使用它们,可以将缺点转化为优点,否则它可能会变得更加繁琐,难以编程或管理,导致应用程序不稳定,处理时间更长,消耗更多带宽,并使应用程序在生产中几乎无法使用。
下面演示的 Calculator 服务代表了这种情况。
public void Add()
{
CalcModel.Add();
}
Public void Sub()
{
CalcModel.Sub();
}
public void Add_Completed(object sender, EventArgs e)
{
//Process Result
}
Public void Sub_Completed(object sender, EventArgs e)
{
//Process Result
}
调用如下:
CalcViewModel.Add();
CalcViewModel.Sub();
在上面的代码中,哪个先返回结果,哪个后返回结果,开发人员无法预测,在所有结果返回之前,很难阻止 UI 线程。很难同步所有项目。非常难以处理服务。服务有内部的 Channel,这是非托管的,需要释放,否则内存消耗会很高。我们经常看到 Silverlight 或 WPF 应用程序挂起 UI 的情况,这使得应用程序在生产环境中非常不可行。
.NET 4.5 中提供的 `async` 和 `await` 关键字,或 2010 的 Async CTP,实现了相同的实现。
public int Task<int> Add(int a, int b)
{
return CalcModel.Add(a, b);
}
public int Task<int>Sub(int a, int b)
{
return CalcModel.Sub(a, b);
}
调用如下:
int res = await CalcViewModel.Add();
int res1 = await CalcViewModel.Sub();
这弥合了异步和同步编程之间的差距,并以高效的方式返回结果,同时解决了上述所有问题。
Dispatcher
WPF 在多线程 Apartment 中运行,由于存在多个线程,很容易出现死锁,并发管理是 WPF 应用程序中的琐事。WPF 中的大多数对象都派生自 DispatcherObject。WPF 基于 Dispatcher 实现的消息系统。这非常类似于 Win32 消息循环。
关于 WPF 中的并发,有两个主要方面。
- Dispatcher
- 线程亲和性
Dispatcher
类用于在其附加线程上执行工作。它有一个工作项队列,负责在 Dispatcher 线程上执行这些工作项。
几乎所有 WPF 元素都有线程亲和性。这意味着对该元素的访问只能来自创建该元素的线程。为此,每个需要线程亲和性的元素都派生自 DispatcherObject
类。该类提供一个名为 Dispatcher 的属性,该属性返回与 WPF 元素关联的 Dispatcher
对象。
如果您想进行异步调用,您应该只使用
Dispatcher.CurrentDispatcher.BeginInvoke(
....
);
即,仅通过 dispatcher,否则会收到运行时错误,指出线程 ID 与更新它的线程不同。
Dispatcher 对象层次结构
在上面,我们可以看到所有对象都派生自 dispatcher 对象,并更有效地处理并发。
使用 Dispatcher
Dispatcher
类提供了一个访问 WPF 中消息泵的入口,并提供了一个将工作路由给 UI 线程进行处理的机制。这对于满足线程亲和性需求是必要的,但由于 UI 线程被阻塞以处理通过 Dispatcher 路由的每项工作,因此重要的是保持 Dispatcher 的工作量小而快捷。最好将较大的用户界面工作分解成小的离散块供 Dispatcher 执行。任何不需要在 UI 线程上完成的工作都应该移到其他线程上进行后台处理。
通常,您会使用 Dispatcher
类将工作项发送到 UI 线程进行处理。例如,如果您想使用 Thread 类在单独的线程上执行一些工作,您可以创建一个 ThreadStart
委托,其中包含一些要在新线程上执行的工作,如下所示。
// The Work to perform on another thread
ThreadStart start = delegate()
{
// ...
// This will throw an exception
// (it's on the wrong thread)
statusText.Text = "From Other Thread";
};
// Create the thread and kick it started!
new Thread(start).Start();
此代码失败,因为 `statusText` 控件(一个 TextBlock
)的 Text 属性不是在 UI 线程上设置的。当代码尝试为 TextBlock
设置 Text 时,TextBlock
类会在内部调用其 VerifyAccess
方法,以确保调用来自 UI 线程。当它确定调用来自不同的线程时,它会抛出异常。那么如何使用 Dispatcher 在 UI 线程上进行调用呢?
Dispatcher
类提供直接在 UI 线程上调用代码的访问权限。下面展示了 Dispatcher 的 Invoke 方法的使用,该方法调用一个名为 SetStatus
的方法来为您更改 TextBlock
的 Text
属性。
// The Work to perform on another thread
ThreadStart start = delegate()
{
// ...
// Sets the Text on a TextBlock Control.
// This will work as its using the dispatcher
Dispatcher.Invoke(DispatcherPriority.Normal,
new Action<string>(SetStatus),
"From Other Thread");
};
// Create the thread and kick it started!
new Thread(start).Start();
Invoke 调用包含三条信息:要执行的项的优先级,描述要执行的工作的委托,以及在第二个参数中描述的要传递给委托的任何参数。通过调用 Invoke,它将委托排队,以便在 UI 线程上执行。使用 Invoke 方法可确保您将在 UI 线程上执行工作,直到工作完成。
作为同步使用 Dispatcher 的替代方法,您可以使用 Dispatcher 的 BeginInvoke
方法异步排队一个 UI 线程的工作项。调用 BeginInvoke
方法会返回一个 DispatcherOperation
类的实例,该类包含有关工作项执行的信息,包括工作项的当前状态和执行结果(一旦工作项完成)。下面展示了 BeginInvoke
方法和 DispatcherOperation
类的使用。
// The Work to perform on another thread
ThreadStart start = delegate()
{
// ...
// This will work as its using the dispatcher
DispatcherOperation op = Dispatcher.BeginInvoke(
DispatcherPriority.Normal,
new Action<string>(SetStatus),
"From Other Thread (Async)");
DispatcherOperationStatus status = op.Status;
while (status != DispatcherOperationStatus.Completed)
{
status = op.Wait(TimeSpan.FromMilliseconds(1000));
if (status == DispatcherOperationStatus.Aborted)
{
// Alert Someone
}
}
};
// Create the thread and kick it started!
new Thread(start).Start();
后台工作者
有很多情况,开发人员会遇到 UI 更新耗时较长的问题,例如从登录屏幕跳转到应用程序,或者点击菜单打开特定模块或屏幕。如果需要显示的数据量很大或需要显示复杂的图形,则会耗费大量时间。
在 WPF 中,通过 Background worker 可以轻松处理这些组件,以提高 UI 响应能力和屏幕加载速度,从而使用户不会看到延迟。
现在您对 Dispatcher 的工作方式有了一定的了解,您可能会惊讶地发现,在大多数情况下您都不会用到它。在 Windows Forms 2.0 中,Microsoft 引入了一个用于非 UI 线程处理的类,以简化用户界面开发人员的开发模型。这个类称为 BackgroundWorker
。下面展示了 BackgroundWorker
类的典型用法。
BackgroundWorker _backgroundWorker = new BackgroundWorker();
...
// Set up the Background Worker Events
_backgroundWorker.DoWork += _backgroundWorker_DoWork;
backgroundWorker.RunWorkerCompleted +=
_backgroundWorker_RunWorkerCompleted;
// Run the Background Worker
_backgroundWorker.RunWorkerAsync(5000);
...
// Worker Method
void _backgroundWorker_DoWork(object sender, DoWorkEventArgs e)
{
// Do something
}
// Completed Method
void _backgroundWorker_RunWorkerCompleted(
object sender,
RunWorkerCompletedEventArgs e)
{
if (e.Cancelled)
{
statusText.Text = "Cancelled";
}
else if (e.Error != null)
{
statusText.Text = "Exception Thrown";
}
else
{
statusText.Text = "Completed";
}
}
BackgroundWorker 组件与 WPF 配合良好,因为它底层使用了 AsyncOperationManager
类,该类又使用 SynchronizationContext
类来处理同步。在 Windows Forms 中,AsyncOperationManager
会传递一个 WindowsFormsSynchronizationContext
类,该类派生自 SynchronizationContext
类。同样,在 ASP.NET 中,它与一个名为 AspNetSynchronizationContext
的 SynchronizationContext
的不同派生类一起工作。这些 SynchronizationContext
派生类知道如何处理跨线程方法调用的同步。
在 WPF 中,此模型通过 DispatcherSynchronizationContext
类得到扩展。通过使用 BackgroundWorker
,Dispatcher 会自动用于调用跨线程方法调用。好消息是,由于您可能已经熟悉这种常见模式,因此您可以在新的 WPF 项目中继续使用 BackgroundWorker
。
数据绑定。
在 WPF 中,作为丰富的客户端之一,它具有大量的图形显示,并且占用更多资源。鉴于如今人们使用高级系统,这已不再是主要问题。但是,如果我们手动附加所有数据并更新 UI,那将是至关重要的因素,并将对性能产生巨大影响,这也可能是 WPF 的一个主要因素。应该注意的是,在 WPF 中,几乎所有的 UI 元素都必须通过数据绑定进行更新,以获得更好的性能。
本节包含以下子节。
通常,每个绑定都包含这四个组件:绑定目标对象、目标属性、绑定源和绑定源中要使用的值路径。例如,如果要将 TextBox
的内容绑定到 Employee 对象的 Name 属性,则目标对象是 TextBox
,目标属性是 Text
属性,要使用的值是 Name,源对象是 Employee 对象。
目标属性必须是依赖属性。大多数 UIElement 属性是依赖属性,大多数非只读的依赖属性默认支持数据绑定。(只有 DependencyObject 类型可以定义依赖属性,并且所有 UIElement 都派生自 DependencyObject。)
尽管图中未指明,但应注意,绑定源对象不限于自定义 CLR 对象。WPF 数据绑定支持 CLR 对象和 XML 形式的数据。举例来说,您的绑定源可以是 UIElement
、任何列表对象、与 ADO.NET 数据或 Web 服务关联的 CLR 对象,或者是包含您的 XML 数据的 XmlNode。有关更多信息,请参阅 Binding Sources Overview。
在阅读其他软件开发工具包 (SDK) 主题时,重要的是要记住,在建立绑定时,您是将绑定目标绑定到绑定源。例如,如果您使用数据绑定在 ListBox
中显示一些底层 XML 数据,那么您是将 ListBox
绑定到 XML 数据。
要建立绑定,您需要使用 Binding
对象。本主题的其余部分将讨论与 Binding
对象相关的许多概念以及一些属性和用法。
数据流方向
如前所述,并且如上图中的箭头所示,绑定的数据流可以从绑定目标流向绑定源(例如,当用户编辑 TextBox
的值时,源值会更改),并且/或者从绑定源流向绑定目标(例如,当绑定源中的更改更新您的 TextBox
内容时),前提是绑定源提供了适当的通知。
您可能希望您的应用程序能够使用户更改数据并将其传播回源对象。或者,您可能不希望允许用户更新源数据。您可以通过设置绑定对象的 Mode
属性来控制这一点。下图说明了不同类型的数据流。
OneWay
绑定会导致源属性的更改自动更新目标属性,但目标属性的更改不会传播回源属性。如果被绑定的控件隐含地是只读的,则此类型的绑定是合适的。例如,您可能绑定到像股票行情自动收录器这样的源,或者您的目标属性没有提供用于进行更改的控件接口,例如数据绑定的表格背景色。如果不需要监视目标属性的更改,使用 OneWay
绑定模式可以避免 TwoWay
绑定模式的开销。
TwoWay
绑定会导致源属性或目标属性的更改自动更新另一个。此类型的绑定适用于可编辑表单或其他完全交互式 UI 场景。大多数属性默认值为 OneWay
绑定,但一些依赖属性(通常是用户可编辑控件的属性,如 TextBox
的 Text
属性和 CheckBox
的 IsChecked
属性)默认值为 TwoWay
绑定。通过编程方式确定依赖属性默认绑定为单向还是双向的方法是使用 GetMetadata
获取该属性的元数据,然后检查 BindsTwoWayByDefault
属性的布尔值。
OneWayToSource
是 OneWay
绑定的反向操作;当目标属性更改时,它会更新源属性。一个示例场景是,如果您只需要从 UI 重新评估源值。
图中未显示 OneTime
绑定,它会导致源属性初始化目标属性,但后续更改不会传播。这意味着如果数据上下文发生更改或数据上下文中的对象发生更改,则更改不会反映在目标属性中。此类型的绑定适用于您正在使用的数据,这些数据要么是当前状态的快照,要么是真正静态的数据。如果您想使用来自源属性的某个值初始化目标属性,并且数据上下文是未知的,那么此类型的绑定也很有用。这本质上是 OneWay
绑定的简化形式,在源值不更改的情况下可以提供更好的性能。
请注意,要检测源更改(适用于 OneWay
和 TwoWay
绑定),源必须实现合适的属性更改通知机制,例如 INotifyPropertyChanged。有关 INotifyPropertyChanged
实现的示例,请参阅 How to: Implement Property Change Notification。
Mode
属性页提供了有关绑定模式的更多信息,以及如何指定绑定方向的示例。
什么触发源更新?
TwoWay
或 OneWayToSource
绑定会侦听目标属性中的更改并将它们传播回源。这称为更新源。例如,您可能会编辑 TextBox 的文本以更改底层源值。如上一节所述,数据流的方向由绑定的 Mode
属性的值决定。
但是,当您编辑文本时,您的源值是否会更新,或者在您完成编辑文本并将鼠标移开 TextBox 后是否会更新?绑定中的 UpdateSourceTrigger
属性决定了什么会触发源的更新。下图中的右箭头点说明了 UpdateSourceTrigger
属性的作用。
如果 UpdateSourceTrigger
的值为 PropertyChanged
,则 TwoWay
或 OneWayToSource
绑定的右箭头指向的值将在目标属性更改时立即更新。但是,如果 UpdateSourceTrigger
的值为 LostFocus
,则该值仅在目标属性失去焦点时才使用新值进行更新。
与 Mode
属性类似,不同的依赖属性具有不同的默认 UpdateSourceTrigger
值。大多数依赖属性的默认值为 PropertyChanged
,而 TextBox.Text
属性的默认值为 LostFocus
。这意味着源更新通常发生在目标属性更改时,这对于 CheckBox
和其他简单控件来说是没问题的。但是,对于文本字段,每次击键后更新会降低性能,并且剥夺了用户在提交新值之前通过退格键修复输入错误的机会。这就是为什么 Text
属性的默认值为 LostFocus
而不是 PropertyChanged
。
有关如何查找依赖属性默认 UpdateSourceTrigger
值的详细信息,请参阅 UpdateSourceTrigger
属性页。
下表以 TextBox 为例,为每个 UpdateSourceTrigger
值提供了一个示例场景:
UpdateSourceTrigger 值 |
源值何时更新 |
TextBox 的示例场景 |
LostFocus (TextBox.Text 的默认值) |
当 TextBox 控件失去焦点时 |
与验证逻辑关联的 TextBox(请参阅数据验证部分) |
PropertyChanged |
当您在 TextBox 中键入时 |
聊天室窗口中的 TextBox 控件 |
Explicit (显式) |
当应用程序调用 |
可编辑表单中的 TextBox 控件(仅在用户单击提交按钮时更新源值) |
页面和导航。
这是 WPF 的一项惊人功能;这是降低内存、提高应用程序速度、减少网络带宽的关键组件,也是任何应用程序在生产环境中取得成功的关键。是任何应用程序在生产环境中取得成功的关键。
网页开发中有许多很棒的功能,如无状态、低内存消耗,因为应用程序中一次只加载一个页面,内容将是 HTML 格式的静态内容,并带有 JS 脚本,这些特性使得 Web 技术如 ASP.NET 成为 Internet 或内联网应用程序的当然选择。
Microsoft 在 WPF 中提供了相同的功能,用户可以选择使用 Page、Navigation 和其他组件来实现所有这些好处,并成为 Web 和 Windows 开发的理想选择。
Windows Presentation Foundation (WPF) 支持浏览器样式导航,可用于两种类型的应用程序:独立应用程序和 XAML 浏览器应用程序 (XBAP)。为了打包导航内容,WPF 提供了 Page
类。您可以通过声明式方式(使用 Hyperlink
)或以编程方式(使用 NavigationService
)从一个 Page
导航到另一个 Page
。WPF 使用 journal 来记住已导航的页面并导航回它们。
Page
、Hyperlink
、NavigationService
和 journal 构成了 WPF 提供的导航支持的核心。本概述将详细探讨这些功能,然后再介绍高级导航支持,包括导航到丢失的 Extensible Application Mark-up Language (XAML) 文件、HTML 文件和对象。
在 WPF 中,您可以导航到多种内容类型,包括 .NET Framework 对象、自定义对象、枚举值、用户控件、XAML 文件和 HTML 文件。但是,您会发现最常见和最方便的打包内容的方式是使用 Page
。此外,Page
还实现了导航特定功能,以增强其外观并简化开发。
使用 Page
,您可以通过类似下面的标记声明式地实现可导航的 XAML 内容页面。
XAML
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" />
在 XAML 标记中实现的 Page
以 Page
作为其根元素,并需要 WPF XML 命名空间声明。Page
元素包含您想要导航和显示的内容。通过设置 Page.Content
属性元素来添加内容,如下面的标记所示。
<Page xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<Page.Content>
<!-- Page Content -->
Hello, Page!
</Page.Content>
</Page>
Page.Content
只能包含一个子元素;在上面的示例中,内容是一个单独的字符串 "Hello, Page!"。实际上,您通常会使用布局控件作为子元素(请参阅 Layout System)来包含和组合您的内容。
Page
元素的子元素被视为 Page
的内容,因此您不需要使用显式的 Page.Content
声明。下面的标记是与前面示例的声明式等价物。
以编程方式导航到 Page 对象
以下示例演示了如何使用 NavigationService
以编程方式导航到 Page
。之所以需要以编程方式导航,是因为要导航到的 Page
只能使用一个非默认构造函数来实例化。带有非默认构造函数的 Page
显示在下面的标记和代码中。
<Page
x:Class="SDKSample.PageWithNonDefaultConstructor"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="PageWithNonDefaultConstructor">
<!-- Content goes here -->
</Page>
using System.Windows.Controls; // Page
namespace SDKSample
{
public partial class PageWithNonDefaultConstructor : Page
{
public PageWithNonDefaultConstructor(string message)
{
InitializeComponent();
this.Content = message;
}
}
}
导航到带有非默认构造函数的 Page
的 Page
显示在下面的标记和代码中。
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.NSNavigationPage">
<Hyperlink Click="hyperlink_Click">
Navigate to Page with Non-Default Constructor
</Hyperlink>
</Page>
using System.Windows; // RoutedEventArgs
using System.Windows.Controls; // Page
using System.Windows.Navigation; // NavigationService
namespace SDKSample
{
public partial class NSNavigationPage : Page
{
public NSNavigationPage()
{
InitializeComponent();
}
void hyperlink_Click(object sender, RoutedEventArgs e)
{
// Instantiate the page to navigate to
PageWithNonDefaultConstructor page = new PageWithNonDefaultConstructor("Hello!");
// Navigate to the page, using the NavigationService
this.NavigationService.Navigate(page);
}
}
}
当此 Page
上的 Hyperlink
被单击时,导航通过实例化要导航到的 Page
(使用非默认构造函数)并调用 NavigationService.Navigate
方法来启动。Navigate
接受一个到 NavigationService
将导航到的对象的引用,而不是一个 pack URI。
使用 Pack URI 进行以编程方式导航
如果需要以编程方式构造 pack URI(例如,当只能在运行时确定 pack URI 时),可以使用 NavigationService.Navigate
方法。这在下面的示例中有所展示。
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.NSUriNavigationPage">
<Hyperlink Click="hyperlink_Click">Navigate to Page by Pack URI</Hyperlink>
</Page>
using System; // Uri, UriKind
using System.Windows; // RoutedEventArgs
using System.Windows.Controls; // Page
using System.Windows.Navigation; // NavigationService
namespace SDKSample
{
public partial class NSUriNavigationPage : Page
{
public NSUriNavigationPage()
{
InitializeComponent();
}
void hyperlink_Click(object sender, RoutedEventArgs e)
{
// Create a pack URI
Uri uri = new Uri("AnotherPage.xaml", UriKind.Relative);
// Get the navigation service that was used to
// navigate to this page, and navigate to
// AnotherPage.xaml
this.NavigationService.Navigate(uri);
}
}
}
刷新当前页面
如果 Page
的 pack URI 与存储在 NavigationService.Source
属性中的 pack URI 相同,则不会下载 Page
。要强制 WPF 重新下载当前页面,您可以调用 NavigationService.Refresh
方法,如下面的示例所示。
<Page
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="SDKSample.NSRefreshNavigationPage">
<Hyperlink Click="hyperlink_Click">Refresh this page</Hyperlink>
</Page>
using System.Windows; // RoutedEventArgs
using System.Windows.Controls; // Page
using System.Windows.Navigation; // NavigationService
namespace SDKSample
{
public partial class NSRefreshNavigationPage : Page
{
...
void hyperlink_Click(object sender, RoutedEventArgs e)
{
// Force WPF to download this page again
this.NavigationService.Refresh();
}
}
导航生命周期
有许多方法可以启动导航,如您所见。当导航启动时,以及在导航进行过程中,您可以使用 NavigationService
实现的以下事件来跟踪和影响导航。
-
Navigating
。当请求新的导航时发生。可用于取消导航。 -
NavigationProgress
。在下载过程中会定期发生,以提供导航进度信息。 -
Navigated
。当页面已被定位和下载时发生。 -
NavigationStopped
。当导航被停止时(通过调用StopLoading
),或当在当前导航正在进行时请求新的导航时发生。 -
NavigationFailed
。当导航到请求的内容时发生错误时发生。 -
LoadCompleted
。当导航到的内容已加载并解析并开始渲染时发生。 -
FragmentNavigation
。当导航到内容片段开始时发生,这会在
- 立即发生,如果所需的片段在当前内容中。
- 如果所需的片段在不同的内容中,则在源内容加载后发生。
导航事件的引发顺序如图所示。
一般而言,Page
不关心这些事件。应用程序更可能关心这些事件,因此 Application
类也引发这些事件。
- Application.Navigating
- Application.NavigationProgress
- Application.Navigated
- Application.NavigationFailed
- Application.NavigationStopped
- Application.LoadCompleted
- Application.FragmentNavigation
每次 NavigationService
引发事件时,Application
类都会引发相应的事件。Frame
和 NavigationWindow
提供相同的事件来检测其各自范围内的导航。
提高性能的编程指南
尽可能使用异步编程来提高性能,如果使用不当,可能会导致性能下降。
明智地编写代码,在必要时使用对象,并且只在范围内声明。
避免以下错误:
bool test = false;
//Some Processing
if (test == true)
{
//some processing
}
我们可以这样写:
if (test)
{
//some processing
}
使用集中式缓存,以便像弱引用一样轻松释放对象。在不需要时通过赋 null 来释放静态对象和事件。谨慎使用 LINQ 或 lambda 表达式,避免编程错误,这类似于 SQL JOIN,如果使用不当可能会耗费很长时间。尝试不使用递归,并实现 while 循环以展示相同的行为。避免使用委托和事件,从 .NET 4.0 及更高版本使用 Action 命令,并在使用后立即释放这些命令。从一开始就构建具有正确设计的应用程序,这有助于后续的维护,并能提供巨大的好处。